weaver-work-cli 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +23 -2
  2. package/dist/internal/e10/auth/commands.js +194 -16
  3. package/dist/internal/e10/auth/xiaoe.js +15 -6
  4. package/dist/internal/e10/context.js +3 -3
  5. package/dist/internal/e10/request-runtime.js +46 -4
  6. package/dist/shortcuts/caizhilian/file-upload/metadata.js +124 -0
  7. package/dist/shortcuts/caizhilian/file-upload/multipart.js +47 -0
  8. package/dist/shortcuts/caizhilian/file-upload/protocol.js +116 -0
  9. package/dist/shortcuts/caizhilian/file-upload/receipt.js +37 -0
  10. package/dist/shortcuts/caizhilian/host.js +13 -5
  11. package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +72 -179
  12. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +701 -4
  13. package/dist/shortcuts/caizhilian/operations/registry.js +5 -1
  14. package/dist/shortcuts/caizhilian/operations/specs.js +52 -0
  15. package/dist/shortcuts/workflow/continuation.js +14 -9
  16. package/dist/shortcuts/workflow/endpoints.js +11 -1
  17. package/dist/shortcuts/workflow/operations/flow-status.js +63 -0
  18. package/dist/shortcuts/workflow/operations/form.js +650 -518
  19. package/dist/shortcuts/workflow/operations/read.js +374 -13
  20. package/dist/shortcuts/workflow/operations/shared.js +3 -0
  21. package/dist/shortcuts/workflow/operations/write.js +5 -5
  22. package/dist/shortcuts/workflow/operations.js +8 -6
  23. package/docs/caizhilian.md +8 -2
  24. package/docs/e10-auth.md +18 -3
  25. package/package.json +1 -1
  26. package/skills/weaver-e10-caizhilian/SKILL.md +8 -3
  27. package/skills/weaver-e10-caizhilian/references/file-upload.md +100 -295
  28. package/skills/weaver-e10-caizhilian/references/folder.md +31 -3
  29. package/skills/weaver-e10-caizhilian/references/source-manifest.json +536 -25
  30. package/skills/weaver-e10-jiuchuanhui/SKILL.md +9 -1
  31. package/skills/weaver-e10-shared/SKILL.md +12 -2
  32. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +21 -3
  33. package/skills/weaver-e10-workflow/SKILL.md +10 -8
  34. package/skills/weaver-e10-workflow/references/source-manifest.json +36 -31
  35. package/skills/weaver-e10-workflow/references/workflow-batch.md +2 -4
  36. package/skills/weaver-e10-workflow/references/workflow-create.md +58 -36
  37. package/skills/weaver-e10-workflow/references/workflow-edit.md +2 -1
  38. package/skills/weaver-e10-workflow/references/workflow-memory.md +2 -2
  39. package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -1
  40. package/skills/weaver-e10-workflow/references/workflow-query.md +9 -7
  41. package/skills/weaver-e10-workflow/references/workflow-share.md +2 -0
@@ -11,6 +11,7 @@ import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CATEGORY, SCHEMA_I
11
11
  import { asString, asInt, asStringArray, asBool, compact, responseData, detailDataOf, toArray, resolveMemoryAppId, withLeadingMeta, STDOUT_SAFE_BYTES, stdoutBytesOf, parseNameWithId, } from './shared.js';
12
12
  import { workflowValidationError, WorkflowCliError, workflowClarificationError, CLARIFICATION_SUBTYPE, } from '../errors.js';
13
13
  import { WORKFLOW_ENDPOINTS } from '../endpoints.js';
14
+ import { flowStatusCondition, FLOW_STATUS_HINT } from './flow-status.js';
14
15
  import { loadSearchIndex, saveSearchIndex, findIndexedItem, readStateFile, writeStateFileBestEffort, } from '../state.js';
15
16
  const ENDPOINT_PREFIX = {
16
17
  todo: 'TODO',
@@ -407,10 +408,10 @@ async function resolveTerm(env, systemKey, rawValue, label) {
407
408
  // 0 命中:只有 --workflow 澄清,其余系统字段一律失败(V11.3 口径)
408
409
  if (systemKey === 'workflowid') {
409
410
  throw workflowClarificationError(clarificationType, text, `${label}「${text}」未匹配到任何对象。请核对名称(是否写错字/少了字),`
410
- + `或提供准确的完整名称(也可直接给工作流 ID)后再查。`);
411
+ + `或提供该对象的完整名称(或其它可用于检索的关键词)后再查。`);
411
412
  }
412
413
  throw workflowValidationError(`${systemKey}_not_found`, `${label}「${text}」未匹配到任何对象(该筛选条件不会生效,继续执行会返回不含该条件的全量数据),`
413
- + '本次查询已停止。请核对名称,或改用「名称(ID)」/ 数字 ID 后重发。');
414
+ + '本次查询已停止。请核对名称,或提供该对象更完整的名称(或其它可用于检索的关键词)后重发。');
414
415
  }
415
416
  const exact = candidates.filter((item) => item.name.trim().toLowerCase() === text.trim().toLowerCase());
416
417
  if (exact.length === 1)
@@ -496,7 +497,7 @@ async function resolveRequestLevel(env, rawValue) {
496
497
  throw workflowClarificationError('system_relate_name2id_requestlevel', text, `紧急程度「${text}」匹配到 ${selected.length} 个等级,请确认要查询的具体等级。`, clarificationCandidates(selected, 10));
497
498
  }
498
499
  throw workflowValidationError('requestlevel_not_found', `紧急程度「${text}」未匹配到任何等级(该筛选条件不会生效),本次查询已停止。`
499
- + `当前环境可选等级:${listed}。请核对后改用「名称(ID)」重发。`);
500
+ + `当前环境可选等级:${listed}。请从上面的等级名称中选一个后重发。`);
500
501
  }
501
502
  /** 时间条件 → formDatas 顶层键(来源:Python MEMORY_DATE_FORM_KEYS)。取值非法时按 V11.3 静默丢弃该条件。 */
502
503
  function applyTimeCondition(formDatas, fieldKey, raw) {
@@ -1012,16 +1013,39 @@ async function buildSearchQuery(env, args, mode) {
1012
1013
  if (requestLevel)
1013
1014
  formDatas.requestlevel = [{ content: requestLevel }];
1014
1015
  // 当前节点 ID
1016
+ // ★ 浏览按钮类条件:只认 `[{term, browserDatas:[{id, name}]}]`。
1017
+ // 旧实现写成 `[{content, term}]`(纯文本形态,那是 requestname/requestmark 的口径)
1018
+ // ⇒ 服务端**静默忽略**该条件、按节点筛选恒不生效——与 `flowstatus` 是同一类移植退化
1019
+ // (对照 `./flow-status.ts` 文件头)。节点是"浏览按钮"字段,不是文本字段。
1020
+ // 来源:V11.3 `e10_workflow.py:10927-10931`,逐字为
1021
+ // `{"term": _node_term, "browserDatas": [{"id": nid, "name": ""} for nid in _node_ids]}`;
1022
+ // 自动匹配路径 `:10637` 是 `[{"term": "in", "browserDatas": nodes}]`,nodes 形如 `{id, name}`。
1023
+ // **browserDatas 里真正起作用的只有 `id`**(用户 2026-09-28 澄清:真实抓包里的 content 等属性
1024
+ // 都非必需);本层拿不到节点名 ⇒ `name` 置空串,与 V11.3 的 `--node-id` 路径完全一致。
1025
+ // 多值合并为「单个条件项 + 多 browserDatas」(同 workflowid 的口径),逐值拆成多个条件项会被
1026
+ // 服务端按 AND 处理 → 恒 0 条。
1015
1027
  const nodeId = asString(args.nodeId);
1016
1028
  if (nodeId) {
1017
1029
  const term = asString(args.nodeIdTerm) === 'notin' ? 'notin' : 'in';
1018
- formDatas.nodeid = [{ content: nodeId, term }];
1030
+ const nodeIds = nodeId.split(',').map((id) => id.trim()).filter(Boolean);
1031
+ if (!nodeIds.length) {
1032
+ throw workflowValidationError('node_id_empty', 'nodeId 必须是节点 ID(可逗号分隔多个),当前只传了分隔符。');
1033
+ }
1034
+ formDatas.nodeid = [{ term, browserDatas: nodeIds.map((id) => ({ id, name: '' })) }];
1019
1035
  }
1020
1036
  // 流程状态
1037
+ // ★ 选项类条件:只认 `[{term, multiContent:[代码]}]`(见 `./flow-status.ts` 的文件头)。
1038
+ // 旧实现写成 `[{content, term}]` ⇒ 服务端静默忽略该条件、按状态筛选恒不生效。
1039
+ // 取值口径(方案 C)=**只认四个代码、不做中文识别**;出现任何非代码取值即报错停止。
1021
1040
  const flowStatus = asString(args.flowStatus);
1022
1041
  if (flowStatus) {
1023
- const term = asString(args.flowStatusTerm) === 'notin' ? 'notin' : 'in';
1024
- formDatas.flowstatus = [{ content: flowStatus, term }];
1042
+ const parsed = flowStatusCondition(flowStatus, args.flowStatusTerm);
1043
+ if (!parsed || parsed.invalid.length) {
1044
+ const offending = parsed?.invalid.length ? parsed.invalid.join('、') : flowStatus;
1045
+ throw workflowValidationError('flow_status_unrecognized', `流程状态「${offending}」不是有效选项代码,本次已停止返回——静默丢弃该条件会退化成全量查询。`
1046
+ + `可选值:${FLOW_STATUS_HINT}。`, { allowedFlowStatus: FLOW_STATUS_HINT });
1047
+ }
1048
+ formDatas.flowstatus = parsed.condition;
1025
1049
  }
1026
1050
  // 时间条件(来源:Python build_system_date_conditions——待办分类下移除操作时间条件:
1027
1051
  // 待办是"还未处理"的流程,操作时间(已操作过的时间)与待办状态语义矛盾)
@@ -1041,12 +1065,12 @@ async function buildSearchQuery(env, args, mode) {
1041
1065
  if (filterFieldsRaw.length) {
1042
1066
  if (!resolvedWorkflows.length) {
1043
1067
  throw workflowValidationError('field_require_workflow', '使用表单字段筛选时必须同时指定工作流(服务端只在该工作流的表单字段内匹配)。'
1044
- + '请先与用户确认要查哪个工作流,再用「名称(ID)」传入 workflow,并附上 filterFields。');
1068
+ + '请先与用户确认要查哪个工作流——只问完整名称或其它可用于检索的关键词,不要向用户索取 ID——再由你按「名称(ID)」传入 workflow,并附上 filterFields。');
1045
1069
  }
1046
1070
  if (resolvedWorkflows.length > 1) {
1047
1071
  throw workflowValidationError('field_require_single_workflow', `表单字段筛选一次只能用于**单个**工作流,当前指定了 ${resolvedWorkflows.length} 个:`
1048
1072
  + `${resolvedWorkflows.map((w) => `${w.name}(${w.id})`).join(';')}。`
1049
- + '请先把候选工作流展示给用户确认,再用「名称(ID)」传入单个 workflow 与 filterFields。', { candidates: resolvedWorkflows.map((w, index) => ({ index: index + 1, id: w.id, name: w.name })) });
1073
+ + '请先把候选工作流展示给用户确认(只问完整名称或其它可用于检索的关键词,不要向用户索取 ID),再由你按「名称(ID)」传入单个 workflow 与 filterFields。', { candidates: resolvedWorkflows.map((w, index) => ({ index: index + 1, id: w.id, name: w.name })) });
1050
1074
  }
1051
1075
  const targetWorkflow = resolvedWorkflows[0];
1052
1076
  const workflowLabel = `${targetWorkflow.name || targetWorkflow.id}(${targetWorkflow.id})`;
@@ -1505,7 +1529,7 @@ const workflowSearch = {
1505
1529
  creatorDept: { type: 'string', description: '发起人部门;纯名称多命中返回澄清候选,0 命中报错' },
1506
1530
  creatorSubcom: { type: 'string', description: '发起人分部;纯名称多命中返回澄清候选,0 命中报错' },
1507
1531
  workflowType: { type: 'string', description: '工作流类型;纯名称多命中返回澄清候选,0 命中报错' },
1508
- workflow: { type: 'array', items: { type: 'string' }, description: '工作流条件,多个 or;支持 "名称(ID)";纯名称未匹配到/匹配到多个且无全名精确匹配时返回澄清(候选见 candidates),确认后用「名称(ID)」重发' },
1532
+ workflow: { type: 'array', items: { type: 'string' }, description: '工作流条件,多个 or;支持 "名称(ID)";纯名称未匹配到/匹配到多个且无全名精确匹配时返回澄清(候选见 candidates),确认后由你按「名称(ID)」重发(只问用户完整名称,不要向用户索取 ID)' },
1509
1533
  workflowTerm: { type: 'string', enum: ['in', 'notin'] },
1510
1534
  requestName: { type: 'string', description: '流程标题 like 模糊' },
1511
1535
  requestNameTerm: { type: 'string', enum: ['in', 'notin', 'eq', 'ne'] },
@@ -1514,9 +1538,9 @@ const workflowSearch = {
1514
1538
  requestLevel: { type: 'string', description: '紧急程度名称(CLI 按当前环境等级列表转 ID);纯名称多命中返回澄清候选,0 命中报错并列出可选等级(对齐 V11.3)' },
1515
1539
  requestId: { type: 'string', description: '按 requestId 精确过滤,逗号分隔' },
1516
1540
  exRequestId: { type: 'string', description: '排除 requestId,逗号分隔' },
1517
- nodeId: { type: 'string', description: '当前节点 ID' },
1541
+ nodeId: { type: 'string', description: '当前节点 ID,逗号分隔可多值(浏览按钮类条件,只能填节点 ID,不能填节点名称)' },
1518
1542
  nodeIdTerm: { type: 'string', enum: ['in', 'notin'] },
1519
- flowStatus: { type: 'string', description: '流程状态' },
1543
+ flowStatus: { type: 'string', description: `流程状态(选项类,多选):${FLOW_STATUS_HINT}` },
1520
1544
  flowStatusTerm: { type: 'string', enum: ['in', 'notin'] },
1521
1545
  createTime: { type: 'string', description: TIME_DESCRIPTIONS.createTime },
1522
1546
  operateTime: { type: 'string', description: TIME_DESCRIPTIONS.operateTime },
@@ -2406,7 +2430,7 @@ function planGroupError(groupName, error) {
2406
2430
  // 与 V11.3 的组内文案同口径(`_plan_system_field_form_datas`:
2407
2431
  // 「plan 内不做澄清,请改用 名称(ID) 形式」)——否则模型会把澄清问题当成"该组没有数据"。
2408
2432
  error: clarification
2409
- ? `${error.message}(plan 内不做澄清:请把候选原样交给用户确认,再用「名称(ID)」重跑该组)`
2433
+ ? `${error.message}(plan 内不做澄清:请把候选原样交给用户确认——只问完整名称或其它可用于检索的关键词,不要向用户索取 ID;确认后由你按「名称(ID)」拼好重跑该组)`
2410
2434
  : error.message,
2411
2435
  ...(clarification
2412
2436
  ? {
@@ -2626,7 +2650,7 @@ async function executePlanSearch(env, plan, planPageSize = 10) {
2626
2650
  'plan 返回为批量编排结果:特殊组/展开组各段 finalMarkdown 均为纯卡片列表(无顶部行),原样粘贴到对应分组标题与一行汇总摘要下',
2627
2651
  '紧急/超时组标题计数:N=该组各槽 total 之和、M=urgent 槽 total、K=overtime 槽 total(0 项省略),如 ## 紧急/超时(2 条|紧急 1 条|超时 1 条)',
2628
2652
  '折叠组(collapsed=true)只返回 total 与 byWorkflow 数量,不展开明细;status=error 的组未取数,按 error 提示修正后可单独作为单条 workflow.search 查询',
2629
- 'status=error 的组:原样把 error 交给用户。若该组同时带 clarificationType/candidates,说明是**名称没对上**(不是"该组没有数据")——请把候选按「名称(ID)」摆给用户确认,再用该组条件单独发一次 workflow.search,禁止当作 0 条渲染或自行挑一个候选',
2653
+ 'status=error 的组:原样把 error 交给用户。若该组同时带 clarificationType/candidates,说明是**名称没对上**(不是"该组没有数据")——请把候选摆给用户确认(只问完整名称或其它可用于检索的关键词,不要向用户索取 ID),确认后由你按「名称(ID)」拼好、用该组条件单独发一次 workflow.search,禁止当作 0 条渲染或自行挑一个候选',
2630
2654
  '任一流程只出现在一个分组:特殊组流程已注入 exRequestIds 从展开组排除',
2631
2655
  '展开组 pureDraft=true:该组卡片全部为本人待提交草稿(draftCount=total),不要渲染其 finalMarkdown(为空),全部并入折叠组「待提交草稿(N 条)」(N=各 pureDraft 组 draftCount 之和)',
2632
2656
  '展开组 pureDraft=false:其 finalMarkdown 已只渲染非草稿卡片(草稿卡片被剔除、数量见 draftCount),直接整段原样粘贴;组内草稿数量在标题/汇总注明即可,无需再逐条展示草稿',
@@ -4888,6 +4912,343 @@ export const workflowReadOperations = [
4888
4912
  workflowPageOpen,
4889
4913
  workflowMemoryPrompt,
4890
4914
  ];
4915
+ // ---------------------------------------------------------------------------
4916
+ // 「必填字段 → 历史可参考值」取数(`workflow.createForm` 内部调用,结果缓存进 continuation)
4917
+ //
4918
+ // 场景:发起流程时,先把**当前用户近一年内、非草稿(审批中+已结束)**的最近 5 条「我发起的」
4919
+ // 流程,连同这些流程里**本次这张表单的字段取值**一起取回来,交给模型判断"能不能直接参考历史值
4920
+ // 补上必填项"——能补就不必去打扰用户。
4921
+ //
4922
+ // V12.1.77 起**调用点从 createRequest 前移到 createForm**(用户 2026-09-29 裁定):
4923
+ // · createForm 阶段就把结果算好,一部分(本表单的**必填字段**)随返回值给模型,让模型**第一次**
4924
+ // `createRequest` 就能带上历史值;
4925
+ // · 完整结果(本表单**全部**字段)缓存进 continuation 载荷,`createRequest` 遇到"新出现的必填
4926
+ // 字段"(createForm 当时还不是必填、多为联动后才变必填)时,直接从缓存里筛,**不再联网**。
4927
+ // · 因此这里支持 `allFormFields`:保留**本表单**所有能取到值的字段,而不只是传入的 targets。
4928
+ //
4929
+ // 两次请求:
4930
+ // ① 列表 `MINE_LIST`(**不调 total**:只要 5 条数据,总数没有意义)
4931
+ // ② 明细 `REQUEST_DETAIL` ×N(**并行**)——`isGetRequestBaseInfo` / `isGetRequestLog` /
4932
+ // `isReadRequest` 全 false、`isGetFormData` true。**`isReadRequest:false` 是关键**:
4933
+ // 一次内部取数不能把用户的历史单标成已读(`workflow.detail` 传的是 true,故不可复用)。
4934
+ //
4935
+ // 稳定性:这是**辅助信息**,失败不能拖垮发起链路 ⇒ 只有认证/网络错误原样上抛,其余错误一律
4936
+ // 降级为 `null`(对模型的文档口径:`null` = 没有可参考的历史数据,请直接向用户澄清)。
4937
+ // ---------------------------------------------------------------------------
4938
+ /** 历史参考数据条数上限。 */
4939
+ export const HISTORY_REQUEST_LIMIT = 5;
4940
+ /** 历史参考数据回溯窗口(天):近一年。 */
4941
+ const HISTORY_LOOKBACK_DAYS = 365;
4942
+ /** 本地日期 → `YYYY-MM-DD`(不用 `toISOString`:那是 UTC,会跨日)。 */
4943
+ function formatLocalDate(date) {
4944
+ const pad = (value) => String(value).padStart(2, '0');
4945
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
4946
+ }
4947
+ /** 原始值是否"空"(空值不能作为参考值)。 */
4948
+ function isEmptyRawFieldValue(value) {
4949
+ if (value === null || value === undefined)
4950
+ return true;
4951
+ if (typeof value === 'string')
4952
+ return value.trim() === '';
4953
+ if (Array.isArray(value))
4954
+ return value.length === 0;
4955
+ if (typeof value === 'object')
4956
+ return Object.keys(value).length === 0;
4957
+ return false;
4958
+ }
4959
+ /**
4960
+ * 明细表单字段的**原始值**(保留对象形态,便于按字段类型格式化成可复用写法)。
4961
+ *
4962
+ * 与 `detailFormFieldValue` 同契约、同候选顺序,唯一区别是**不提前串化**——后者服务详情面板
4963
+ * (只需要可读文本),前者服务历史参考值(还需要 `id` 才能拼出 `名称(id)`)。两处必须同步修改。
4964
+ */
4965
+ function detailFormFieldRawValue(obj, formField) {
4966
+ const dataKey = pickText(formField, 'dataKey', 'fieldKey', 'key');
4967
+ const fieldId = pickText(formField, 'id', 'fieldId');
4968
+ const fieldName = pickText(formField, 'title', 'fieldName', 'name', 'label');
4969
+ const formData = obj?.formData;
4970
+ if (formData && typeof formData === 'object') {
4971
+ for (const key of [dataKey, fieldId, fieldName]) {
4972
+ if (key && key in formData) {
4973
+ const value = formData[key];
4974
+ if (!isEmptyRawFieldValue(value))
4975
+ return value;
4976
+ }
4977
+ }
4978
+ }
4979
+ if (Array.isArray(obj?.dataOptions) && obj.dataOptions.length)
4980
+ return obj.dataOptions;
4981
+ for (const key of ['showValue', 'displayValue', 'fieldValue', 'value', 'content', 'text']) {
4982
+ if (obj && typeof obj === 'object' && key in obj) {
4983
+ const value = obj[key];
4984
+ if (!isEmptyRawFieldValue(value))
4985
+ return value;
4986
+ }
4987
+ }
4988
+ return null;
4989
+ }
4990
+ /** 递归收集明细里的 `fieldId → 原始值`(只记**第一个非空**值,避免日志域的同 id 空值遮蔽真值)。 */
4991
+ function collectHistoryFieldRawValues(node, out, depth = 0) {
4992
+ if (!node || typeof node !== 'object' || depth > 8)
4993
+ return;
4994
+ if (Array.isArray(node)) {
4995
+ for (const item of node)
4996
+ collectHistoryFieldRawValues(item, out, depth + 1);
4997
+ return;
4998
+ }
4999
+ const record = node;
5000
+ const formField = record.formField;
5001
+ if (formField && typeof formField === 'object') {
5002
+ const fieldId = pickText(formField, 'id', 'fieldId');
5003
+ if (fieldId && !out.has(fieldId)) {
5004
+ const raw = detailFormFieldRawValue(record, formField);
5005
+ if (raw !== null)
5006
+ out.set(fieldId, raw);
5007
+ }
5008
+ }
5009
+ for (const key of Object.keys(record)) {
5010
+ const child = record[key];
5011
+ if (child && typeof child === 'object')
5012
+ collectHistoryFieldRawValues(child, out, depth + 1);
5013
+ }
5014
+ }
5015
+ /** 从一个原始值里抽出 `名称 / ID` 对(浏览按钮、选项类字段的值都是这种形态)。 */
5016
+ function historyNameIdPairs(value) {
5017
+ const pairs = [];
5018
+ const visit = (item) => {
5019
+ if (item === null || item === undefined)
5020
+ return;
5021
+ if (Array.isArray(item)) {
5022
+ for (const child of item)
5023
+ visit(child);
5024
+ return;
5025
+ }
5026
+ if (typeof item === 'object') {
5027
+ const name = pickText(item, 'name', 'showName', 'displayValue', 'showValue', 'label', 'title', 'lastName', 'userName', 'content', 'value');
5028
+ const id = pickText(item, 'id', 'optionId', 'selectionId', 'valueKey', 'code', 'key', 'value');
5029
+ if (name || id)
5030
+ pairs.push({ name: name || id, id: id && id !== name ? id : '' });
5031
+ return;
5032
+ }
5033
+ const text = String(item).trim();
5034
+ if (text)
5035
+ pairs.push({ name: text, id: '' });
5036
+ };
5037
+ visit(value);
5038
+ return pairs;
5039
+ }
5040
+ /**
5041
+ * 附件类字段(`FileComponent`)的**可直接复用**历史值。
5042
+ *
5043
+ * 为什么必须保留结构化对象:detail 接口对附件字段返回的本就是
5044
+ * `{formField:{id}, dataOptions:[{optionId, content, type}]}`——`optionId` 是服务端的**附件 ID**、
5045
+ * `type` 是文件类型(图片是 `image`、普通文件是 `file`)、`content` 是文件名。早先这里没有专门分支,
5046
+ * 被 `stringifyDetailValue` 压成"只有文件名的字符串",模型手里没有真 ID,只能拿文件名去拼一个
5047
+ * ⇒ `saveFormData` 落库的附件对象指向不存在的文件(2026-09-30 真机会话实证:报错重试后命令
5048
+ * 不报错、提交成功,但对象是错的)。信息本来就在,必须在格式化这一步留住。
5049
+ *
5050
+ * 任一项缺 `optionId`/`content` 时**不猜**:返回 `null` 让调用方退回纯文本形态(旧口径)。
5051
+ */
5052
+ function historyAttachmentValue(raw, field) {
5053
+ const fieldId = String(field?.id ?? '').trim();
5054
+ if (!fieldId)
5055
+ return null;
5056
+ const options = [];
5057
+ let incomplete = false;
5058
+ const visit = (item) => {
5059
+ if (item === null || item === undefined)
5060
+ return;
5061
+ if (Array.isArray(item)) {
5062
+ for (const child of item)
5063
+ visit(child);
5064
+ return;
5065
+ }
5066
+ if (typeof item !== 'object')
5067
+ return;
5068
+ const optionId = pickText(item, 'optionId', 'fileId', 'fid');
5069
+ const content = pickText(item, 'content', 'fileName', 'name', 'showName', 'label', 'title', 'value');
5070
+ if (!optionId || !content) {
5071
+ incomplete = true;
5072
+ return;
5073
+ }
5074
+ const type = pickText(item, 'type', 'fileType');
5075
+ options.push(type ? { optionId, content, type } : { optionId, content });
5076
+ };
5077
+ visit(raw);
5078
+ if (incomplete || !options.length)
5079
+ return null;
5080
+ return { formField: { id: fieldId }, dataOptions: options };
5081
+ }
5082
+ /** 按字段类型把原始值格式化成"可直接复用"的写法。 */
5083
+ function formatHistoryFieldValue(raw, field) {
5084
+ // 附件类:必须回到结构化对象,否则模型拿不到真 `optionId`/`type`(见 historyAttachmentValue)。
5085
+ if (String(field?.componentKey ?? '').toLowerCase() === 'filecomponent') {
5086
+ const attachment = historyAttachmentValue(raw, field);
5087
+ if (attachment)
5088
+ return attachment;
5089
+ }
5090
+ if (field?.isBrowser || field?.isSelect) {
5091
+ const pairs = historyNameIdPairs(raw);
5092
+ if (pairs.length) {
5093
+ return pairs.map((pair) => (pair.id ? `${pair.name}(${pair.id})` : pair.name)).join('、');
5094
+ }
5095
+ }
5096
+ return stringifyDetailValue(raw);
5097
+ }
5098
+ /**
5099
+ * 取「历史流程可参考字段值」。返回 `null` 表示没有可参考的历史数据。
5100
+ *
5101
+ * - `fields`:当前表单布局里的字段清单(`context.fields`,用来判断字段是不是浏览按钮/选项类,
5102
+ * 并在 `allFormFields` 档收口缓存范围——只有本表单的字段 id 会被保留)
5103
+ * - `targets`:本次要取值的字段(`{id,name}` 或 `missingFields` 同形;`systemField` 项会被忽略)
5104
+ * - `options.allFormFields`:见 `HistoryRequestDataOptions`
5105
+ *
5106
+ * 只保留**至少命中 1 个可参考字段值**的历史流程;系统字段(流程标题/紧急程度等)不参与。
5107
+ */
5108
+ export async function buildHistoryRequestData(env, fields, targets, options = {}) {
5109
+ const targetList = toArray(targets)
5110
+ .filter((item) => !item?.systemField && String(item?.id ?? '').trim())
5111
+ .map((item) => ({ id: String(item.id).trim(), name: String(item.name ?? item.id) }));
5112
+ // 普通档必须真的有字段要取;缓存档允许 targets 为空(表单本身没有静态必填时,
5113
+ // 缓存仍要建——条件必填是在 createRequest 阶段才现身的,那时已经没有联网取数的机会了)。
5114
+ if (!targetList.length && !options.allFormFields)
5115
+ return null;
5116
+ const fieldMeta = new Map();
5117
+ for (const field of toArray(fields))
5118
+ fieldMeta.set(String(field?.id ?? ''), field);
5119
+ // 取值顺序 = 「本表单布局顺序」(缓存档)或 targets 顺序(普通档)。
5120
+ // 缓存档用布局顺序是为了结果稳定可 diff;但只保留本表单的字段 id,避免把别张表单的字段带进来。
5121
+ const orderedIds = options.allFormFields
5122
+ ? [...fieldMeta.keys()]
5123
+ : targetList.map((t) => t.id);
5124
+ const nameOf = new Map(targetList.map((t) => [t.id, t.name]));
5125
+ try {
5126
+ const now = new Date();
5127
+ const start = new Date(now.getTime() - HISTORY_LOOKBACK_DAYS * 24 * 60 * 60 * 1000);
5128
+ const historyWorkflowId = String(options.workflowId ?? '').trim();
5129
+ const listResponse = await env.host.workflowRequest({
5130
+ method: 'POST',
5131
+ path: WORKFLOW_ENDPOINTS.MINE_LIST,
5132
+ operation: 'workflow.createRequest.history',
5133
+ body: {
5134
+ params: { tabid: DEFAULT_TABID.mine, dimension: 'mine', flowSpareType: 1, outType: 0 },
5135
+ searchParams: {
5136
+ formDatas: {
5137
+ // 非草稿 = 「未结束」+「已结束」(选项类系统条件必须是 multiContent,见 flow-status.ts;
5138
+ // 取值必须用**代码**——该模块只认四个选项代码,不吃中文)
5139
+ flowstatus: flowStatusCondition('notFinish,finish')?.condition ?? [],
5140
+ createdatetime: [{
5141
+ dateContent: [formatLocalDate(start), formatLocalDate(now)],
5142
+ term: 'like',
5143
+ }],
5144
+ // 只取**同一张表单**的历史(字段 id 是表单级的,跨表单必零交集,见 options.workflowId)。
5145
+ // 形态只认 `browserDatas`(workflowid 属"浏览按钮类"):传 `multiContent` 会被服务端
5146
+ // **静默忽略**(HTTP 200、无报错、结果不变)——2026-09-29 实测踩过。
5147
+ ...(historyWorkflowId
5148
+ ? { workflowid: [{ term: 'in', browserDatas: [{ id: historyWorkflowId, name: '' }] }] }
5149
+ : {}),
5150
+ },
5151
+ type: 'and',
5152
+ initSearch: false,
5153
+ },
5154
+ customDisplayPage: { current: 1, size: HISTORY_REQUEST_LIMIT },
5155
+ sortParams: [{ orderkey: SORT_OPTIONS['发起时间'], sortType: 'desc' }],
5156
+ },
5157
+ });
5158
+ const listed = normalizeListResponse(listResponse, 'mine', CATEGORY_NAMES.mine);
5159
+ const items = toArray(listed.items).slice(0, HISTORY_REQUEST_LIMIT);
5160
+ const requestIds = items.map((item) => String(item?.requestId ?? '').trim()).filter(Boolean);
5161
+ if (!requestIds.length)
5162
+ return null;
5163
+ const session = await env.host.getSession();
5164
+ const details = await Promise.all(requestIds.map(async (requestId) => {
5165
+ const payload = {
5166
+ requestId,
5167
+ isGetRequestBaseInfo: false,
5168
+ isGetRequestLog: false,
5169
+ isGetFormData: true,
5170
+ isReadRequest: false,
5171
+ };
5172
+ if (session.userId)
5173
+ payload.userId = session.userId;
5174
+ const response = await env.host.workflowRequest({
5175
+ method: 'POST',
5176
+ path: WORKFLOW_ENDPOINTS.REQUEST_DETAIL,
5177
+ operation: 'workflow.createRequest.history.detail',
5178
+ body: payload,
5179
+ });
5180
+ return detailDataOf(response);
5181
+ }));
5182
+ const result = [];
5183
+ for (let index = 0; index < details.length; index += 1) {
5184
+ const data = details[index] || {};
5185
+ const rawById = new Map();
5186
+ collectHistoryFieldRawValues(data, rawById);
5187
+ const fieldValues = [];
5188
+ for (const fieldId of orderedIds) {
5189
+ const raw = rawById.get(fieldId);
5190
+ if (raw === undefined || raw === null)
5191
+ continue;
5192
+ const value = formatHistoryFieldValue(raw, fieldMeta.get(fieldId));
5193
+ // 附件类返回的是对象 ⇒ 不能再用 `String(value).trim()` 判空(那会得到 "[object Object]")。
5194
+ if (isEmptyRawFieldValue(value))
5195
+ continue;
5196
+ fieldValues.push({
5197
+ fieldId,
5198
+ fieldName: nameOf.get(fieldId) || String(fieldMeta.get(fieldId)?.name ?? fieldId),
5199
+ value,
5200
+ });
5201
+ }
5202
+ if (!fieldValues.length)
5203
+ continue;
5204
+ const base = data.workflowBaseInfo && typeof data.workflowBaseInfo === 'object' ? data.workflowBaseInfo : {};
5205
+ const listedItem = items[index] || {};
5206
+ result.push({
5207
+ requestId: requestIds[index],
5208
+ requestName: pickText(base, 'requestName', 'requestNameTitle')
5209
+ || String(listedItem.flowName ?? ''),
5210
+ createTime: pickText(base, 'createDateTime', 'createTime')
5211
+ || String(listedItem.createdAt ?? ''),
5212
+ fieldValues,
5213
+ });
5214
+ }
5215
+ return result.length ? result : null;
5216
+ }
5217
+ catch (error) {
5218
+ rethrowAuthOrNetworkError(error);
5219
+ return null;
5220
+ }
5221
+ }
5222
+ /**
5223
+ * 从历史取数结果里筛出**指定字段**的取值(不改动传入对象)。
5224
+ *
5225
+ * 用途:`createForm` 缓存的是一份"本表单全部字段"的宽结果,`createRequest` 按当时真实的
5226
+ * 缺失字段(或"新出现的必填字段")在这里做二次筛选。
5227
+ *
5228
+ * 语义与取数同源:**某个字段一个值都没命中的历史流程整条剔除**;一条都不剩 ⇒ 返回 `null`
5229
+ * (不塞空数组,避免模型误以为"有数据")。
5230
+ */
5231
+ export function filterHistoryRequestData(records, fieldIds) {
5232
+ const wanted = new Set();
5233
+ for (const id of fieldIds) {
5234
+ const text = String(id ?? '').trim();
5235
+ if (text)
5236
+ wanted.add(text);
5237
+ }
5238
+ if (!wanted.size)
5239
+ return null;
5240
+ const result = [];
5241
+ for (const record of toArray(records)) {
5242
+ if (!record || typeof record !== 'object')
5243
+ continue;
5244
+ const fieldValues = toArray(record.fieldValues)
5245
+ .filter((value) => value && wanted.has(String(value.fieldId ?? '')));
5246
+ if (!fieldValues.length)
5247
+ continue;
5248
+ result.push({ ...record, fieldValues });
5249
+ }
5250
+ return result.length ? result : null;
5251
+ }
4891
5252
  /**
4892
5253
  * 供**批量范围驱动**复用:该大分类的「全部」子分类 tabid(todo=0 / done=50 / mine=100 / all=300)。
4893
5254
  *
@@ -181,6 +181,9 @@ export function withLeadingMeta(meta, body) {
181
181
  * 白花好几轮工具往返(实测某次会话多花 4 轮、约 54 s)。与其让宿主事后截,不如 CLI 主动落盘。
182
182
  *
183
183
  * **量这个线必须用 `stdoutBytesOf()`(美化口径)**,不能用压缩口径 —— 见该函数注释。
184
+ *
185
+ * 2026-09-29 备注:本线曾按用户要求临时抬到 `Number.MAX_SAFE_INTEGER`(去掉本地输出限制),
186
+ * 用于实测"命令输出到底会不会被宿主截断";**实测已完成,本线已按原值恢复**(=24,000)。
184
187
  */
185
188
  export const STDOUT_SAFE_BYTES = 24_000;
186
189
  /** 结果序列化后的字节数(**压缩**口径,用作相对大小参考)。 */
@@ -126,12 +126,12 @@ async function resolveReceiverNames(env, receiverName) {
126
126
  ids.push(candidates[0].id);
127
127
  }
128
128
  else if (candidates.length > 1) {
129
- throw workflowValidationError('receiver_ambiguous', `接收人「${part}」匹配到多个候选,请确认后回传 "名称(ID)"`, {
129
+ throw workflowValidationError('receiver_ambiguous', `接收人「${part}」匹配到多个候选,请把 candidates 原样交给用户确认是哪一个——只问完整姓名或其它可用于检索的关键词,不要向用户索取 ID;用户确认后由你按 "姓名(ID)" 拼好并重发同一条命令`, {
130
130
  candidates: candidates.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })),
131
131
  });
132
132
  }
133
133
  else {
134
- throw workflowValidationError('receiver_not_found', `未匹配到接收人「${part}」,请提供准确姓名或 "名称(ID)"`, {
134
+ throw workflowValidationError('receiver_not_found', `未匹配到接收人「${part}」,请提供该接收人的完整姓名(或其它可用于检索的关键词)`, {
135
135
  receiverName: part,
136
136
  });
137
137
  }
@@ -557,7 +557,7 @@ async function resolveShareTarget(env, obj, minSec, maxSec) {
557
557
  const exact = candidates.filter((c) => c.name.trim().toLowerCase() === name.toLowerCase());
558
558
  const selected = exact.length === 1 ? exact : candidates;
559
559
  if (!selected.length) {
560
- throw workflowValidationError('share_target_not_found', `共享对象「${name}(${scope})」未匹配到任何候选,请核对名称或改用 "名称(ID)"`, { shareType: scope, name });
560
+ throw workflowValidationError('share_target_not_found', `共享对象「${name}(${scope})」未匹配到任何候选,请核对名称,或提供该对象更完整的名称(或其它可用于检索的关键词)`, { shareType: scope, name });
561
561
  }
562
562
  if (selected.length !== 1) {
563
563
  // 多命中不是失败而是"需要澄清":按 V11.3 语义以成功态返回候选(模型展示给用户后按
@@ -639,7 +639,7 @@ async function resolveForwardReceiver(env, obj) {
639
639
  const exact = candidates.filter((c) => c.name.trim().toLowerCase() === name.toLowerCase());
640
640
  const selected = exact.length === 1 ? exact : candidates;
641
641
  if (selected.length !== 1) {
642
- throw workflowValidationError('forward_receiver_ambiguous', `转发接收人「${name}(${rtype})」匹配到 ${selected.length} 个候选,请确认后回传 "名称(ID)"`, { candidates: selected.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })) });
642
+ throw workflowValidationError('forward_receiver_ambiguous', `转发接收人「${name}(${rtype})」匹配到 ${selected.length} 个候选,请把 candidates 原样交给用户确认是哪一个——只问完整姓名或其它可用于检索的关键词,不要向用户索取 ID;用户确认后由你按 "姓名(ID)" 拼好并重发同一条命令`, { candidates: selected.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })) });
643
643
  }
644
644
  return { _entityType: rtype, id: selected[0].id };
645
645
  }
@@ -1216,7 +1216,7 @@ const workflowSharePrepare = {
1216
1216
  clarificationQuestion: `共享对象「${err.payload.name}」匹配到多个候选,请确认要共享给哪一个。`,
1217
1217
  candidates: err.payload.candidates,
1218
1218
  displayRules: [
1219
- 'needClarification=true 时原样展示候选,等用户确认后用 "名称(ID)" 重新调用 workflow.sharePrepare',
1219
+ 'needClarification=true 时原样展示候选,等用户确认后用 "名称(ID)" 重新调用 workflow.sharePrepare(只问用户完整名称或其它可用于检索的关键词,不要向用户索取 ID)',
1220
1220
  '候选序号与显示名称保持原样,不要自行重建查询条件',
1221
1221
  ],
1222
1222
  });
@@ -85,20 +85,22 @@ function clarificationEnvelope(error, operation) {
85
85
  function clarificationNote(reason, candidateCount) {
86
86
  const base = '这不是失败,也**不代表查询已执行**:本次查询已停止,没有返回任何流程数据。';
87
87
  if (reason === 'workflow_not_exist') {
88
- return base + '请如实告知用户:该工作流 ID 不存在(可能已删除或被停用),'
89
- + '让用户核对 ID 或改用流程名称重新指定。**不要**说成"没有可发起的工作流"这类模糊说法——'
90
- + '用户给的是一个具体 ID,问题就出在这个 ID 上。';
88
+ return base + '请如实告知用户:该工作流不存在(可能已删除或被停用),'
89
+ + '请用户改用该流程的完整名称(或其它可用于检索的关键词)重新指定——**不要向用户索取 ID**。'
90
+ + '**不要**说成"没有可发起的工作流"这类模糊说法——'
91
+ + '问题就出在这个目标上。';
91
92
  }
92
93
  if (reason === 'no_create_right') {
93
94
  return base + '请如实告知用户:该流程**存在**,但当前账号**没有发起权限**。'
94
95
  + '**必须明确说出「存在但没有发起权限」**,绝不能转述成"流程不存在";'
95
96
  + '并给出下一步:改用其他可发起的流程,或联系管理员为该流程授予发起权限。';
96
97
  }
97
- return base + '请把澄清问题与候选**原样**交给用户确认(逐条列出「名称(ID)」),'
98
- + '拿到确认后用「名称(ID)」格式重发同一条命令。'
98
+ return base + '请把澄清问题与候选**原样**交给用户确认(逐条列出候选名称),'
99
+ + '拿到确认后由你按「名称(ID)」把 ID 拼好、重发同一条命令——'
100
+ + '**只问用户该对象的完整名称或其它可用于检索的关键词,不要向用户索取 ID**。'
99
101
  + '**不要**把本次结果转述成「没有匹配的流程」「查询结果为 0 条」或自行挑一个候选继续查。'
100
102
  + (candidateCount === 0
101
- ? '当前没有任何候选——请让用户给出准确的完整名称(或纠正名称里的错别字)后重发。'
103
+ ? '当前没有任何候选——请让用户给出该对象的完整名称(或纠正名称里的错别字、换其它可用于检索的关键词)后重发。'
102
104
  : '');
103
105
  }
104
106
  /**
@@ -293,12 +293,18 @@ weaver-work-cli --json caizhilian run caizhilian.attachment.download --input-jso
293
293
  | 加解密设置 | `caizhilian.setting-encrypt.*` | 8 项加解密配置 |
294
294
  | 存储设置 | `caizhilian.setting-storage.*` | 存储实例与规则管理 |
295
295
  | 文件采集 | `caizhilian.file-collection.*` | FTP/SFTP 文件采集任务管理 |
296
- | 文件上传 | `caizhilian.file-upload.*` | 本地文件上传到文件服务 |
296
+ | 文件上传 | `caizhilian.file-upload.*` | 秒传、普通/分片流式上传、终态校验与私有回执 |
297
297
 
298
298
  详细用法参见各模块的 reference 文档。
299
299
 
300
+ 文件上传协议代码位于 `src/shortcuts/caizhilian/file-upload/`,按文件/上下文校验、流式 multipart、
301
+ 上传策略和回执分为四个模块;`operations/handlers/file-upload.ts` 接入现有 schema 与确认链。
302
+ 协议采用 `weaver-file-upload` 的上传契约,认证继续由 CLI 框架统一托管。
303
+ 新增的 `test/business/caizhilian-file-upload.test.ts` 使用本机 HTTP 服务验证实际传输格式和失败语义;
304
+ 本机验证不代表真实 E10 租户已验证。
305
+
300
306
  ## 验证状态
301
307
 
302
308
  - 已通过:`tsc --noEmit`、`npm run build`、`caizhilian schema` / `caizhilian operations` 实际输出、`test/business/caizhilian-operations.test.ts`(24 项)与 `test/business/caizhilian-command.test.ts`(5 项)。
303
309
  - **未过检测器**:本次生成没有可用的 Skill 检测器(`validate_skill.py`),因此交付状态标注为「未过检」。
304
- - **真实 E10 端到端目前只覆盖 `folder.query-by-permission`**:2026-09-21 在真实租户验证了按权限反查(默认共享 / 文档权限 × 人员 / 部门),并修复了 `object_ids` 转 number 导致的精度丢失缺陷;创建、共享、上传、下载与比对链路仍为离线契约级(fake host),未在真实 E10 环境跑通。
310
+ - **真实 E10 端到端目前只覆盖 `folder.query-by-permission`**:2026-09-21 在真实租户验证了按权限反查(默认共享 / 文档权限 × 人员 / 部门),并修复了 `object_ids` 转 number 导致的精度丢失缺陷;创建、共享、上传、下载与比对链路仍为离线契约级(fake host),未在真实 E10 环境跑通。
package/docs/e10-auth.md CHANGED
@@ -34,20 +34,35 @@ weaver-work-cli auth profile use PROFILE_NAME
34
34
  weaver-work-cli auth export-weaver-env
35
35
  ```
36
36
 
37
- `auth login` 和 `auth oidc` 都走 E10 OAuth2.0 浏览器授权登录,本地默认监听
37
+ `auth login` 和 `auth oidc` 都严格走 E10 OAuth2.0 浏览器授权登录,不会读取或消费 XiaoE 统一登录凭据。本地默认监听
38
38
  `http://127.0.0.1:19800/callback`。如端口被占用,可以使用
39
39
  `--port 19801` 指定其他端口;如需要区分客户端来源,可以使用
40
40
  `--agent_type <value>`。未显式传入时,CLI 会依次读取 `WEAVER_AGENT_TYPE`、
41
41
  `WORKBUDDY_AGENT_TYPE`、`CODEX_AGENT_TYPE`,再根据 `WORKBUDDY_*` / `CODEX_*`
42
42
  环境变量推断当前宿主,最后回落为 `weaver-work-cli`。浏览器授权地址会同时携带
43
43
  `agent_type=<value>` 和 `from=<value>`,其中 `from` 用于 E10 授权确认页展示客户端名称。
44
-
45
- XiaoE 宿主环境中,`auth login` / `auth oidc` / `auth xiaoe` 先使用平台注入的 `XIAOE_USER_TOKEN` + `XIAOE_DOMAIN` 换取 ETEAMSID(沙箱/Linux 环境唯一可用来源)。这里的 `XIAOE_DOMAIN` 是 XiaoE/erpa 换票服务地址,不是 E10 登录地址。Node 主路径调用与 `xiaoe_env.py` / `tokens.py` 相同的接口,校验业务 `code`(`"0000"` 或 `200`)及 `data.eteamId`,保留系统 CA 校验,并只跟随同源重定向或同主机 HTTP→HTTPS 升级。Node 路径失败时,CLI 会在内部执行 npm 包自带的 `scripts/xiaoe_env.py` 兼容路径;它只依赖 Python 标准库,凭证 stdout 由 CLI 内部消费,不进入 Agent 上下文,也不会关闭 TLS 证书校验。之后才依次读取宿主 `xiaoeUserData/tokens.py`、桌面 XiaoE 客户端本地会话(macOS `~/Library/Application Support/XiaoE/Robots/<id>/user_settings.json`,Windows `%LOCALAPPDATA%/XiaoE/Robots/<id>/user_settings.json`)和 `WEAVER_XIAOE_*` ETEAMSID 映射。宿主不会注入 ETEAMSID 环境变量,CLI 也不读取 `XIAOE_ETEAMSID`。
44
+ 授权完成后的本地回调页会为已识别的桌面宿主显示应用打开弹层。CLI 内置 WorkBuddy
45
+ (`workbuddy://`)、智能小 E(`xiaoe-client://`)和 Codex(`codex://`)映射;其他
46
+ 智能体在 `agent_type` 本身是合法协议名时,默认按 `<agent_type>://` 唤起。Windows/macOS
47
+ 安装包需要注册对应协议。弹层通过一次性令牌请求
48
+ 本地 CLI,再由 CLI 调用系统协议处理器,避免浏览器直接访问自定义协议时出现不可定制的
49
+ Chrome/Edge 原生确认框。宿主需要返回特定会话时,可以传
50
+ `--agent-return-url <自定义协议 URL>`,或注入 `WEAVER_AGENT_RETURN_URL`(命令行优先)。
51
+ 该显式地址也用于协议名与 `agent_type` 不一致的客户端。
52
+ 返回地址只接受桌面应用自定义协议,不接受 `http(s)`、脚本或本地文件 URL。“始终打开”
53
+ 偏好保存在当前本地回调地址的浏览器存储中。本地 CLI 打开通道默认保留 15 分钟;通道
54
+ 结束后页面会自动退回浏览器自定义协议打开方式,不直接显示网络请求错误。
55
+ 智能小 E 统一登录成功时不会经过浏览器;只有统一登录不可用并降级为桌面浏览器 OAuth 时,
56
+ 才显示“返回智能小 E”弹层。
57
+
58
+ 只有当前智能体身份明确为 XiaoE 时,`auth xiaoe` 和未显式指定认证来源的业务命令才先使用平台注入的 `XIAOE_USER_TOKEN` + `XIAOE_DOMAIN` 换取 ETEAMSID(沙箱/Linux 环境唯一可用来源)。这里的 `XIAOE_DOMAIN` 是 XiaoE/erpa 换票服务地址,不是 E10 登录地址。Node 主路径调用与 `xiaoe_env.py` / `tokens.py` 相同的接口,校验业务 `code`(`"0000"` 或 `200`)及 `data.eteamId`,保留系统 CA 校验,并只跟随同源重定向或同主机 HTTP→HTTPS 升级。Node 路径失败时,CLI 会在内部执行 npm 包自带的 `scripts/xiaoe_env.py` 兼容路径;它只依赖 Python 标准库,凭证 stdout 由 CLI 内部消费,不进入 Agent 上下文,也不会关闭 TLS 证书校验。之后才依次读取宿主 `xiaoeUserData/tokens.py`、桌面 XiaoE 客户端本地会话(macOS `~/Library/Application Support/XiaoE/Robots/<id>/user_settings.json`,Windows `%LOCALAPPDATA%/XiaoE/Robots/<id>/user_settings.json`)和 `WEAVER_XIAOE_*` ETEAMSID 映射。宿主不会注入 ETEAMSID 环境变量,CLI 也不读取 `XIAOE_ETEAMSID`。仅本机存在这些文件或配置不足以触发 XiaoE 登录。
46
59
 
47
60
  E10 目标地址按“显式 `--base-url` → 专用 `WEAVER_XIAOE_*` E10 配置/映射 → 当前 profile → `https://www.e-cology.com.cn`”解析,绝不会把 `XIAOE_DOMAIN` 当作 E10 地址。凭证可用时,CLI 直接保存登录态,保存的 `agentType` 固定为 `XiaoE`;业务命令在未显式指定 `--auth` / `--profile` 时,也会自动优先尝试 XiaoE 统一登录。统一登录失败后,`auth xiaoe` 仅在 macOS/Windows 桌面宿主自动降级浏览器 OAuth;Web/Linux 环境直接返回 `authentication/xiaoe_web_credential_unavailable`,不再启动无法回调的 localhost 登录。用户明确希望浏览器授权且宿主能够接收本地回调时,可以显式运行 `auth login`。
48
61
 
49
62
  脚本兼容链路会先自动发现桌面 XiaoE 客户端内置 Python(macOS 应用包与 Windows 按用户/全局安装均支持),再回退到系统 `python3`、`/usr/bin/python3`、`python`;Windows 的系统回退顺序为 `python`、`python3`、`py`。可用 `WEAVER_XIAOE_TOKEN_SCRIPT_PYTHON` 精确指定解释器,用 `WEAVER_XIAOE_TOKEN_SCRIPT_PATH` 指定桌面宿主脚本路径(旧名 `XIAOE_TOKEN_SCRIPT_PYTHON` / `XIAOE_TOKEN_SCRIPT_PATH` 仍兼容)。CLI 自带的 `xiaoe_env.py` 位于 npm 包 `scripts/`,不会放入 `weaver-e10-shared` Skill;Skill 包禁止携带或直接执行 Token 脚本。`XIAOE_` 是平台保留前缀,本 CLI 的自定义变量统一使用 `WEAVER_XIAOE_*`。
50
63
 
64
+ 上述“业务命令自动优先尝试”仅适用于当前智能体身份明确为 XiaoE 的进程。WorkBuddy、Codex 及其他智能体不会因为本机存在 XiaoE 文件或配置而进入该分支,并必须使用 `auth login --base-url <url> --agent_type <agent_type>` 的浏览器 OAuth 流程。
65
+
51
66
  无法使用浏览器回调时,可以用 `auth set` 直接写入浏览器中复制到的
52
67
  `ETEAMSID`。命令会调用 `teamsCheck` 解析当前用户、租户和组织信息,并写入
53
68
  E10 兼容的 auth 文件。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weaver-work-cli",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
4
4
  "description": "面向泛微工作工具链的可扩展 TypeScript CLI 基础框架(E10 业票通等能力)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",