my-tasks.js
1 import { cli, Strategy } from '@jackwener/opencli/registry'; 2 import { CliError } from '@jackwener/opencli/errors'; 3 import { gotoOnesHome, onesFetchInPage, resolveOnesUserUuid } from './common.js'; 4 import { enrichPeekEntriesWithDetails } from './enrich-tasks.js'; 5 import { resolveTaskListLabels } from './resolve-labels.js'; 6 import { defaultPeekBody, flattenPeekGroups, mapTaskEntry, parsePeekLimit, } from './task-helpers.js'; 7 /** 文档示例里「负责人」常用 field004;与顶层 assign 在不同部署上二选一有效 */ 8 function queryAssign(userUuid) { 9 return { must: [{ equal: { assign: userUuid } }] }; 10 } 11 function queryAssignField004(userUuid) { 12 return { must: [{ in: { 'field_values.field004': [userUuid] } }] }; 13 } 14 function queryOwner(userUuid) { 15 return { must: [{ equal: { owner: userUuid } }] }; 16 } 17 function dedupeByUuid(entries) { 18 const seen = new Set(); 19 const out = []; 20 for (const e of entries) { 21 const id = String(e.uuid ?? ''); 22 if (!id || seen.has(id)) 23 continue; 24 seen.add(id); 25 out.push(e); 26 } 27 return out; 28 } 29 async function peekTasks(page, team, query, cap) { 30 const path = `team/${team}/filters/peek`; 31 const body = defaultPeekBody(query); 32 const parsed = (await onesFetchInPage(page, path, { 33 method: 'POST', 34 body: JSON.stringify(body), 35 skipGoto: true, 36 })); 37 return flattenPeekGroups(parsed, cap); 38 } 39 cli({ 40 site: 'ones', 41 name: 'my-tasks', 42 description: 'ONES — my work items (filters/peek + strict must query). Default: assignee=me. Use --mode if your site uses field004 for assignee.', 43 domain: 'ones.cn', 44 strategy: Strategy.COOKIE, 45 browser: true, 46 navigateBefore: false, 47 args: [ 48 { 49 name: 'team', 50 type: 'str', 51 required: false, 52 positional: true, 53 help: 'Team UUID from URL …/team/<uuid>/…, or set ONES_TEAM_UUID', 54 }, 55 { 56 name: 'limit', 57 type: 'int', 58 default: 100, 59 help: 'Max rows (default 100, max 500)', 60 }, 61 { 62 name: 'mode', 63 type: 'str', 64 default: 'assign', 65 choices: ['assign', 'field004', 'owner', 'both'], 66 help: 'assign=负责人(顶层 assign);field004=负责人(筛选器示例里的 field004);owner=创建者;both=负责人∪创建者(两次 peek 去重)', 67 }, 68 ], 69 columns: ['title', 'status', 'project', 'uuid', 'updated', '工时'], 70 func: async (page, kwargs) => { 71 const team = kwargs.team?.trim() || 72 process.env.ONES_TEAM_UUID?.trim() || 73 process.env.ONES_TEAM_ID?.trim(); 74 if (!team) { 75 throw new CliError('CONFIG', 'team UUID required', 'Pass team from URL …/team/<team>/… or set ONES_TEAM_UUID.'); 76 } 77 const limit = parsePeekLimit(kwargs.limit, 100); 78 const mode = String(kwargs.mode ?? 'assign'); 79 await gotoOnesHome(page); 80 const userUuid = await resolveOnesUserUuid(page, { skipGoto: true }); 81 let entries = []; 82 if (mode === 'both') { 83 const cap = Math.min(500, limit * 2); 84 const asAssign = await peekTasks(page, team, queryAssign(userUuid), cap); 85 const asOwner = await peekTasks(page, team, queryOwner(userUuid), cap); 86 entries = dedupeByUuid([...asAssign, ...asOwner]).slice(0, limit); 87 } 88 else { 89 const queryByMode = () => { 90 switch (mode) { 91 case 'field004': 92 return queryAssignField004(userUuid); 93 case 'owner': 94 return queryOwner(userUuid); 95 case 'assign': 96 default: 97 return queryAssign(userUuid); 98 } 99 }; 100 const primary = queryByMode(); 101 try { 102 entries = await peekTasks(page, team, primary, limit); 103 } 104 catch (e) { 105 const msg = e instanceof Error ? e.message : ''; 106 const canFallback = mode === 'assign' && 107 (msg.includes('ServerError') || msg.includes('801') || msg.includes('Params is invalid')); 108 if (canFallback) { 109 entries = await peekTasks(page, team, queryAssignField004(userUuid), limit); 110 } 111 else { 112 throw e; 113 } 114 } 115 } 116 const enriched = await enrichPeekEntriesWithDetails(page, team, entries, true); 117 const labels = await resolveTaskListLabels(page, team, enriched, true); 118 return enriched.map((e) => mapTaskEntry(e, labels)); 119 }, 120 });