/ clis / ones / my-tasks.js
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  });