weaver-work-cli 0.1.7 → 0.1.8

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.
@@ -3939,13 +3939,69 @@ async function commandEditSave(args, env) {
3939
3939
  // ---------------------------------------------------------------------------
3940
3940
  // createList(查询可发起工作流)
3941
3941
  // ---------------------------------------------------------------------------
3942
+ /**
3943
+ * `createList` 大输出保护。
3944
+ *
3945
+ * 未传 `name` / `workflowId` 时本命令返回**当前用户全部可发起工作流**,流程多的租户动辄数千个
3946
+ * (线上实测某租户 3,862 个、美化后约 460 KB),远超宿主的单次输出线(≈28 KB)⇒ 会被截成
3947
+ * 2048 B 预览,模型只能自己写脚本去读文件(线上会话即如此绕行)。
3948
+ *
3949
+ * 对齐同文件的 `compactFormOutput` 与 `read.ts` 的 `enforceSearchStdoutBudget`:超安全线时把
3950
+ * 完整清单落盘,stdout 只保留前 `CREATE_LIST_INLINE_LIMIT` 条,并在**最前面**给出 `resultFile`
3951
+ * (被截断也看得见);落盘不可用时保持全量(宁大不丢数据),只补一句说明。
3952
+ *
3953
+ * 注意:本命令的 `SELECTED`(1 条)/ `NOT_FOUND`(0 条)分支体积天然很小,无需过此函数。
3954
+ */
3955
+ const CREATE_LIST_INLINE_LIMIT = 100;
3956
+ async function compactCreateListOutput(payload, env) {
3957
+ const bytes = stdoutBytesOf(payload);
3958
+ if (bytes <= STDOUT_SAFE_BYTES)
3959
+ return payload;
3960
+ const workflows = toArray(payload.workflows);
3961
+ const resultFile = await writeStateFileBestEffort('last_create_list.json', JSON.stringify(payload, null, 2), env.ctx);
3962
+ if (!resultFile) {
3963
+ return {
3964
+ ...payload,
3965
+ outputBytes: bytes,
3966
+ largeOutputNotice: `可发起工作流较多(${workflows.length} 个)且本地落盘不可用,已按原样返回全量 workflows。`
3967
+ + '请改用 name 或 workflowId 精确定位,不要整份读入上下文。',
3968
+ };
3969
+ }
3970
+ return withLeadingMeta({
3971
+ resultFile,
3972
+ persistedResult: true,
3973
+ outputBytes: bytes,
3974
+ workflowsTotal: workflows.length,
3975
+ workflowsTruncated: true,
3976
+ largeOutputNotice: `可发起工作流较多(${workflows.length} 个),完整清单已写入 resultFile;`
3977
+ + `stdout 只保留前 ${CREATE_LIST_INLINE_LIMIT} 条。`
3978
+ + '需要更多候选时从 resultFile 按需提取(不要整份读入上下文)——更推荐直接传 `name`(按名称匹配)'
3979
+ + '或 `workflowId`(精确校验)来定位,避免拉全量。读取本输出请一次读完,不要用 head/tail 截断。',
3980
+ }, {
3981
+ ...payload,
3982
+ workflows: workflows.slice(0, CREATE_LIST_INLINE_LIMIT),
3983
+ });
3984
+ }
3942
3985
  async function commandCreateList(args, env) {
3986
+ const workflowId = String(args.workflowId ?? '').trim();
3987
+ const name = String(args.name ?? '').trim();
3988
+ // `name` 与 `workflowId` 都不传时**直接返回提示、不发请求**:这种调用(线上会话出现过
3989
+ // `--input-json '{}'`)只会拿到当前用户的全部可发起工作流(线上实测 3,862 条),既定位不到
3990
+ // 用户要的流程,体积也必然超输出线 —— 与其拉下来再落盘/截断兜底,不如根本不发这一次请求。
3991
+ if (!workflowId && !name) {
3992
+ return workflowSuccess('workflow.createList', {
3993
+ status: 'MISSING_QUERY',
3994
+ message: '未提供 name 或 workflowId,已跳过查询。不传这两个参数时只能拉取当前用户的全部可发起工作流,'
3995
+ + '既无法定位到用户要的流程、体积也远超单次输出上限。'
3996
+ + '请先用业务流程名称(name)做模糊匹配,或直接传已知的 workflowId 做精确校验。',
3997
+ workflows: [],
3998
+ });
3999
+ }
3943
4000
  const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, { wfpBelongTypeCode: '1', loadOfsFlow: '0', clientCtrl: 1 });
3944
4001
  // requestJson 内部已解包一层(responseData),此处不可再取 .data
3945
4002
  const data = (response && typeof response === 'object') ? response : null;
3946
4003
  if (!data || typeof data !== 'object')
3947
4004
  throw workflowApiError('format_error', '工作流列表返回 data 格式错误');
3948
- const workflowId = String(args.workflowId ?? '').trim();
3949
4005
  if (workflowId) {
3950
4006
  const authorized = simplifyWorkflows(data);
3951
4007
  const exact = authorized.filter((item) => String(item.id ?? '').trim() === workflowId);
@@ -3959,15 +4015,15 @@ async function commandCreateList(args, env) {
3959
4015
  }
3960
4016
  return workflowSuccess('workflow.createList', {
3961
4017
  status: 'NOT_FOUND',
3962
- query: args.name || '',
4018
+ query: name,
3963
4019
  workflowId,
3964
4020
  message: '记忆中的 workflowId 不在当前用户可发起工作流列表中,可能已无发起权限或工作流已停用。',
3965
4021
  workflows: [],
3966
4022
  });
3967
4023
  }
3968
- const result = simplifyWorkflows(data, String(args.name ?? ''));
4024
+ const result = simplifyWorkflows(data, name);
3969
4025
  if (!result.length) {
3970
- return workflowSuccess('workflow.createList', { status: 'NOT_FOUND', query: args.name || '', workflows: [] });
4026
+ return workflowSuccess('workflow.createList', { status: 'NOT_FOUND', query: name, workflows: [] });
3971
4027
  }
3972
4028
  if (result.length === 1) {
3973
4029
  return workflowSuccess('workflow.createList', { status: 'SELECTED', workflow: result[0] });
@@ -3976,7 +4032,7 @@ async function commandCreateList(args, env) {
3976
4032
  // 对上「总部费用报销 / 总部费用报销流程 / 总部费用报销(自定义)」),就直接选中它,不让用户
3977
4033
  // 为"是不是要多打几个字"再选一次。这与查询侧 `resolveTerm` 的候选优选是**同一套判据**。
3978
4034
  // 只有在"没有任何候选与所写完全一致"(或完全一致的有多个、仍是歧义)时才请用户选择。
3979
- const keyword = String(args.name ?? '').trim().toLowerCase();
4035
+ const keyword = name.toLowerCase();
3980
4036
  const exact = keyword
3981
4037
  ? result.filter((item) => String(item.workflowname ?? '').trim().toLowerCase() === keyword)
3982
4038
  : [];
@@ -3985,16 +4041,16 @@ async function commandCreateList(args, env) {
3985
4041
  status: 'SELECTED',
3986
4042
  workflow: exact[0],
3987
4043
  selectionSource: 'exactName',
3988
- message: `名称与「${args.name}」完全一致的工作流只有 1 个,已直接选中,无需用户选择。`,
4044
+ message: `名称与「${name}」完全一致的工作流只有 1 个,已直接选中,无需用户选择。`,
3989
4045
  });
3990
4046
  }
3991
4047
  const selected = exact.length ? exact : result;
3992
- return workflowSuccess('workflow.createList', {
4048
+ return workflowSuccess('workflow.createList', await compactCreateListOutput({
3993
4049
  status: 'NEEDS_SELECTION',
3994
4050
  message: '匹配到多个工作流,且其中没有与所写名称完全一致的,请让用户选择后再继续。',
3995
- query: args.name || '',
4051
+ query: name,
3996
4052
  workflows: selected,
3997
- });
4053
+ }, env));
3998
4054
  }
3999
4055
  function simplifyWorkflows(data, name = '') {
4000
4056
  const groups = Array.isArray(data.listDataGroup) ? data.listDataGroup : [];
@@ -4040,8 +4096,8 @@ const workflowCreateList = {
4040
4096
  risk: 'read',
4041
4097
  requiresConfirmation: false,
4042
4098
  inputSchema: schemaOf({
4043
- name: { type: 'string', description: '工作流名称关键字(模糊匹配)' },
4044
- workflowId: { type: 'string', description: '记忆中已确认的工作流 ID;传入时按当前用户可发起列表精确校验并优先选中' },
4099
+ name: { type: 'string', description: '工作流名称关键字(模糊匹配)。**与 workflowId 至少传一个**——两个都不传时本命令不做查询,直接返回 status=MISSING_QUERY 的提示(不传参数只能拉取全部可发起工作流,无法定位用户要的流程)' },
4100
+ workflowId: { type: 'string', description: '记忆中已确认的工作流 ID;传入时按当前用户可发起列表精确校验并优先选中。**与 name 至少传一个**' },
4045
4101
  }, { required: [] }),
4046
4102
  handler: (args, env) => commandCreateList(args, env),
4047
4103
  };
@@ -8,7 +8,7 @@
8
8
  // (saveSearchIndex / findIndexedItem,由 CLI 托管,本文件不读写索引文件);
9
9
  // 查看/操作类 operation 允许用 requestId 或 group+index 定位(findIndexedItem)。
10
10
  import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CATEGORY, SCHEMA_INDEX, SCHEMA_PAGE_SIZE, } from './types.js';
11
- import { asString, asInt, asStringArray, asBool, compact, responseData, toArray, resolveMemoryAppId, withLeadingMeta, STDOUT_SAFE_BYTES, stdoutBytesOf, } from './shared.js';
11
+ import { asString, asInt, asStringArray, asBool, compact, responseData, detailDataOf, toArray, resolveMemoryAppId, withLeadingMeta, STDOUT_SAFE_BYTES, stdoutBytesOf, } from './shared.js';
12
12
  import { workflowValidationError, WorkflowCliError, workflowClarificationError, CLARIFICATION_SUBTYPE, } from '../errors.js';
13
13
  import { WORKFLOW_ENDPOINTS } from '../endpoints.js';
14
14
  import { loadSearchIndex, saveSearchIndex, findIndexedItem, readStateFile, writeStateFileBestEffort, } from '../state.js';
@@ -1448,7 +1448,6 @@ const workflowSearch = {
1448
1448
  tabid: { type: 'string', description: '细分子类 tabid' },
1449
1449
  tabName: { type: 'string', description: '细分子类名称' },
1450
1450
  subCategory: { type: 'string', description: '细分子类名称或 tabid' },
1451
- keyword: { type: 'array', items: { type: 'string' }, description: '实体关键词(辅助解析)' },
1452
1451
  creator: { type: 'string', description: '发起人姓名,支持 "名称(ID)";纯名称多命中返回澄清候选,0 命中报错(对齐 V11.3)' },
1453
1452
  creatorDept: { type: 'string', description: '发起人部门;纯名称多命中返回澄清候选,0 命中报错' },
1454
1453
  creatorSubcom: { type: 'string', description: '发起人分部;纯名称多命中返回澄清候选,0 命中报错' },
@@ -2993,8 +2992,12 @@ const workflowDetail = {
2993
2992
  },
2994
2993
  };
2995
2994
  function normalizeDetailResponse(response) {
2996
- const record = responseData(response);
2997
- const data = record?.data && typeof record.data === 'object' ? record.data : {};
2995
+ // ★ 层级:`detailDataOf` 负责"`responseData()` 已解一层 `data`"这件事(真机实测响应为
2996
+ // `{message, data:{workflowBaseInfo, otherInfo, formData}}`、**无内层 `data`**,与 V11.3 同口径)。
2997
+ // 旧实现在这里又取了一次 `.data` ⇒ 实际拿到 `response.data.data`(不存在)⇒ `workflow.detail` 与
2998
+ // `workflow.summary` **恒空**(basicInfo 全空、formData.available=false),`workflow.write` 的操作
2999
+ // 门禁也因同源写法读不到 `mapItem` ⇒ 拦截失效。详见 `detailDataOf` 的注释与 PITFALLS A.50。
3000
+ const data = detailDataOf(response);
2998
3001
  const base = data.workflowBaseInfo && typeof data.workflowBaseInfo === 'object' ? data.workflowBaseInfo : {};
2999
3002
  const other = data.otherInfo && typeof data.otherInfo === 'object' ? data.otherInfo : {};
3000
3003
  const currentNodes = toArray(base.currentNodeList ?? []);
@@ -3010,11 +3013,40 @@ function normalizeDetailResponse(response) {
3010
3013
  creatorName: pickText(base, 'creatorLastName', 'createrName', 'creatorName'),
3011
3014
  createdAt: pickText(base, 'createDateTime', 'createTime'),
3012
3015
  currentNode: currentNode || pickText(base, 'currentNodeName', 'nodeName'),
3013
- requestLevel: pickText(base, 'requestLevelShow', 'requestlevelShow', 'requestLevelTitle', 'requestlevelTitle') || asString(base.requestLevel),
3014
- flowStatus: pickText(base, 'flowstatusShow', 'status') || asString(base.flowStatus),
3016
+ // 这两项的口径与列表侧(本文件 `:1396`/`:1432`)一致,也与 V11.3 `normalize_workflow_detail_response`
3017
+ // (`:3716`/`:3721`)逐字对齐:接口的 `flowStatus` 是 `{code,desc,...}` 对象、`requestLevel` 是
3018
+ // `0/1/2` 编码,**必须先过归一化函数**。旧写法直接 `asString(...)` ⇒ 详情面板里流程状态显示成
3019
+ // "[object Object]"、紧急程度显示成裸数字。
3020
+ // 这两项的口径与列表侧(本文件 `:1396`/`:1432`)一致,也与 V11.3 `normalize_workflow_detail_response`
3021
+ // (`:3716`/`:3721`)逐字对齐:接口的 `flowStatus` 是 `{code,desc,...}` 对象、`requestLevel` 是
3022
+ // `0/1/2` 编码,**必须先过归一化函数**。旧写法直接 `asString(...)` ⇒ 详情面板里流程状态显示成
3023
+ // "[object Object]"、紧急程度显示成裸数字。
3024
+ requestLevel: requestLevelText(pickText(base, 'requestLevelShow', 'requestlevelShow', 'requestLevelTitle', 'requestlevelTitle')
3025
+ || base.requestLevel),
3026
+ flowStatus: flowStatusText(base.flowStatus) || pickText(base, 'flowstatusShow', 'status'),
3015
3027
  };
3016
3028
  const formFields = extractFormFields(data);
3017
3029
  const formLogs = extractRequestLogs(data);
3030
+ // ★ 静默失败防线:接口成功但什么都没返回时,不能产出 `warnings: []` + 全空 basicInfo ——
3031
+ // 模型看到的是一个"成功但空白"的结果,既不知道是查不到、没权限还是参数错,容易当"流程是空的"。
3032
+ // (2026-09-19 追查:当时线上"detail 恒空"的**真根因**是 `responseData()` 之后又多解了一层
3033
+ // `data`(见 `detailDataOf`),已修;本 warnings 保留,作为**真的取不到**时的兜底。)
3034
+ const detailEmpty = !basicInfo.requestTitle
3035
+ && !basicInfo.workflowName
3036
+ && !basicInfo.requestMark
3037
+ && !basicInfo.createdAt
3038
+ && !basicInfo.creatorName
3039
+ && !basicInfo.currentNode
3040
+ && formFields.length === 0;
3041
+ const warnings = [];
3042
+ if (detailEmpty) {
3043
+ warnings.push('接口未返回该流程的任何详情内容(流程标题/编号/发起人/发起时间/当前节点/表单字段全为空)。'
3044
+ + '这不代表"流程是空的",也不代表流程一定不存在——最可能是这个 requestId 在本环境查不到对应流程'
3045
+ + '(编号抄错、来自其他环境、已被删除)。'
3046
+ + '请先用 `workflow.search` / `workflow.createList` 在本环境确认该流程可见、并直接从那一次的'
3047
+ + '`cards[].requestId` 取值,再重试;若确认流程存在却仍取不到,请如实告知用户"详情不可见",'
3048
+ + '不要编造流程内容。');
3049
+ }
3018
3050
  return {
3019
3051
  basicInfo,
3020
3052
  formData: {
@@ -3023,7 +3055,7 @@ function normalizeDetailResponse(response) {
3023
3055
  fields: formFields,
3024
3056
  },
3025
3057
  requestLog: { available: formLogs.length > 0, items: formLogs },
3026
- warnings: [],
3058
+ warnings,
3027
3059
  };
3028
3060
  }
3029
3061
  // 去 HTML 标签 + 反转义(来源:Python strip_html,L681)。模块级共用。
@@ -3164,82 +3196,508 @@ function extractFormFields(data) {
3164
3196
  }
3165
3197
  return fields;
3166
3198
  }
3199
+ // ---------------------------------------------------------------------------
3200
+ // 签字意见取值工具族(★ 2026-09-19 按 V11.3 逐函数对齐)
3201
+ //
3202
+ // V11.3 的链路:`extract_request_log_items(response)` → 取 `response["data"]` → 交给
3203
+ // `collect_detail_logs_from_obj` **递归收集**(list 逐项递归;dict 归一化后继续下钻 15 个候选键)。
3204
+ // V12 早期版本把"递归"退化成"枚举 4 条固定路径"(`data.list` / `data.records` …),而
3205
+ // `getRequestLog` 的真实响应是 `{message:{errcode,errmsg}, data:[…]}` —— **`data` 直接是数组**
3206
+ // ⇒ 四条路径全 miss ⇒ 签字意见恒为空。真机探针实测同一份响应:V11.3 解出 8 条、V12 解出 0 条。
3207
+ // 归一化侧的 `display_text_from_value` 一族(对象取值 / 用户名映射 / 时间戳格式化 / 接收人解析)
3208
+ // V12 同样缺失 ⇒ 一并补齐。
3209
+ // ---------------------------------------------------------------------------
3210
+ /** 展示文本候选键(来源:V11.3 `DISPLAY_TEXT_KEYS`)。 */
3211
+ const LOG_DISPLAY_TEXT_KEYS = [
3212
+ 'showValue', 'displayValue', 'name', 'showName', 'lastName', 'lastname', 'userName',
3213
+ 'username', 'label', 'title', 'text', 'content', 'value',
3214
+ ];
3215
+ /** 用户 ID 候选键(来源:V11.3 `USER_ID_KEYS`)。 */
3216
+ const LOG_USER_ID_KEYS = [
3217
+ 'id', 'userId', 'userid', 'uid', 'resourceId', 'resourceid', 'masterId', 'masterid',
3218
+ 'identityId', 'identityid',
3219
+ ];
3220
+ /** 日志容器候选键(来源:V11.3 `collect_detail_logs_from_obj`)。 */
3221
+ const LOG_CONTAINER_KEYS = [
3222
+ 'requestLogList', 'requestLogs', 'logs', 'logList', 'operatorList', 'signList', 'remarkList',
3223
+ 'opinionList', 'records', 'rows', 'dataList', 'datas', 'children', 'items', 'list',
3224
+ ];
3225
+ /** 详情里"日志容器"的键名特征(来源:V11.3 `DETAIL_LOG_CONTAINER_PATTERN`)。 */
3226
+ const LOG_CONTAINER_KEY_PATTERN = /log|日志|remark|opinion|sign|operate|审批|签字|流转/iu;
3227
+ const LOG_NODE_KEYS = [
3228
+ 'nodeName', 'node', 'activityName', 'activityname', 'activity', 'activityInfo', 'operateNode',
3229
+ 'operatenode', 'operateNodeName', 'operatenodename', 'nodeNameShow', 'nodeShow',
3230
+ ];
3231
+ const LOG_OPERATOR_KEYS = [
3232
+ 'operatorName', 'operator', 'operatorUser', 'operatorUserName', 'operatorLastName',
3233
+ 'operatorLastname', 'operatorShow', 'operateUser', 'operateUserName', 'operateusername',
3234
+ 'operateUserShow', 'operateBy', 'operateByName', 'operaterName', 'operater', 'handlerUser',
3235
+ 'handlerUserName', 'userName', 'username', 'handlerName', 'handler', 'handleUser',
3236
+ 'handleUserName', 'receiverName', 'receiver', 'receiverUser', 'receiverUserName', 'resourceName',
3237
+ 'resource', 'createUser', 'createUserName', 'creator', 'creatorName', 'creatorLastName',
3238
+ 'lastName', 'lastname',
3239
+ ];
3240
+ const LOG_TIME_KEYS = [
3241
+ 'operateTime', 'operatetime', 'operateDateTime', 'operatedatetime', 'operatorTime',
3242
+ 'handlerTime', 'handleTime', 'receiveTime', 'createTime', 'createdTime', 'createdatetime',
3243
+ 'createDateTime', 'dateTime', 'datetime', 'time',
3244
+ ];
3245
+ const LOG_ACTION_KEYS = [
3246
+ 'commentName', 'commentname', 'operateType', 'operateTypeName', 'operateTypeShow', 'operateName',
3247
+ 'operateNameShow', 'operation', 'operationName', 'operationType', 'operationTypeName', 'action',
3248
+ 'actionName', 'actionType', 'dealType', 'dealTypeName', 'handleType', 'handleTypeName',
3249
+ 'submitType', 'submitTypeName', 'typeName', 'typeShow', 'status', 'statusName', 'statusShow',
3250
+ 'logType', 'requestLogType',
3251
+ ];
3252
+ const LOG_OPINION_KEYS = [
3253
+ 'opinion', 'remark', 'remarks', 'signRemark', 'signremark', 'content', 'comment', 'message',
3254
+ 'requestLogContent', 'commentContent', 'commentcontent',
3255
+ ];
3256
+ const LOG_RECEIVER_ID_KEYS = [
3257
+ 'receivedPersonIds', 'receivedpersonids', 'receivedPersonId', 'receivePersonIds', 'receiverIds',
3258
+ 'recipientIds',
3259
+ ];
3260
+ const LOG_RECEIVER_TEXT_KEYS = [
3261
+ 'receivedPersons', 'receivedPersonNames', 'receiverNames', 'receivers', 'recipients',
3262
+ ];
3263
+ /** 宽松取键:先精确匹配、再忽略大小写(来源:V11.3 `pick_raw`)。 */
3264
+ function pickRawLoose(source, ...keys) {
3265
+ if (!source || typeof source !== 'object' || Array.isArray(source))
3266
+ return undefined;
3267
+ const record = source;
3268
+ for (const key of keys) {
3269
+ if (key in record)
3270
+ return record[key];
3271
+ }
3272
+ const lowered = new Map();
3273
+ for (const [key, value] of Object.entries(record))
3274
+ lowered.set(key.toLowerCase(), value);
3275
+ for (const key of keys) {
3276
+ const hit = lowered.get(key.toLowerCase());
3277
+ if (hit !== undefined)
3278
+ return hit;
3279
+ }
3280
+ return undefined;
3281
+ }
3282
+ /**
3283
+ * 任意值 → 展示文本(来源:V11.3 `display_text_from_value`)。
3284
+ *
3285
+ * 这是"签名里放的是对象"也能读出来的关键:先按 `DISPLAY_TEXT_KEYS` 取(`commentor` 对象因此
3286
+ * 取到 `name`),再下钻常见包裹键,最后才 `stripHtml`;深度上限 4,避免环。
3287
+ */
3288
+ function displayTextFromValue(value, depth = 0) {
3289
+ if (value === null || value === undefined)
3290
+ return '';
3291
+ if (typeof value === 'string' && !value.trim())
3292
+ return '';
3293
+ if (Array.isArray(value) && value.length === 0)
3294
+ return '';
3295
+ if (depth > 4)
3296
+ return '';
3297
+ if (Array.isArray(value)) {
3298
+ return value.map((item) => displayTextFromValue(item, depth + 1)).filter(Boolean).join('、');
3299
+ }
3300
+ if (typeof value === 'object') {
3301
+ const record = value;
3302
+ if (Object.keys(record).length === 0)
3303
+ return '';
3304
+ // ★ 必须走 `pickRawLoose`(大小写不敏感):实测 `userDataMap` 的键是 `UserName`(大写 U+N),
3305
+ // 而候选键写的是 `userName`/`username` —— 用 `in` 精确匹配会取不到,接收人就只能显示裸 ID。
3306
+ for (const key of LOG_DISPLAY_TEXT_KEYS) {
3307
+ const raw = pickRawLoose(record, key);
3308
+ if (raw === undefined)
3309
+ continue;
3310
+ const text = displayTextFromValue(raw, depth + 1);
3311
+ if (text)
3312
+ return text;
3313
+ }
3314
+ for (const key of ['browserDatas', 'resource', 'resourceInfo', 'hrmResource', 'user', 'userInfo', 'userBaseInfo', 'baseInfo', 'person', 'identity', 'operator', 'handler', 'receiver', 'creator', 'data']) {
3315
+ const raw = pickRawLoose(record, key);
3316
+ if (raw === undefined)
3317
+ continue;
3318
+ const text = displayTextFromValue(raw, depth + 1);
3319
+ if (text)
3320
+ return text;
3321
+ }
3322
+ return '';
3323
+ }
3324
+ return stripHtml(value);
3325
+ }
3326
+ /** 按候选键取展示文本(来源:V11.3 `pick_display_text`)。 */
3327
+ function pickDisplayText(source, ...keys) {
3328
+ for (const key of keys) {
3329
+ const text = displayTextFromValue(pickRawLoose(source, key));
3330
+ if (text)
3331
+ return text;
3332
+ }
3333
+ return '';
3334
+ }
3335
+ /** 值 → 去重 ID 列表(来源:V11.3 `value_to_id_list`,ID 串按 `,,;;空白` 切分)。 */
3336
+ function valueToIdList(value) {
3337
+ const ids = [];
3338
+ const pushText = (text) => {
3339
+ for (const part of stripHtml(text).split(/[,,;;\s]+/u)) {
3340
+ if (part)
3341
+ ids.push(part);
3342
+ }
3343
+ };
3344
+ const push = (input) => {
3345
+ if (input === null || input === undefined)
3346
+ return;
3347
+ if (Array.isArray(input)) {
3348
+ input.forEach(push);
3349
+ return;
3350
+ }
3351
+ if (typeof input === 'object') {
3352
+ const record = input;
3353
+ for (const key of [...LOG_USER_ID_KEYS, 'value']) {
3354
+ const raw = pickRawLoose(record, key);
3355
+ if (raw !== null && raw !== undefined && raw !== '') {
3356
+ push(raw);
3357
+ return;
3358
+ }
3359
+ }
3360
+ return;
3361
+ }
3362
+ pushText(String(input));
3363
+ };
3364
+ push(value);
3365
+ return [...new Set(ids)];
3366
+ }
3367
+ /** 用户映射里按 id 查姓名(来源:V11.3 `user_name_from_map`)。 */
3368
+ function userNameFromMap(map, userId) {
3369
+ if (!map || !userId)
3370
+ return '';
3371
+ const direct = displayTextFromValue(map[userId]);
3372
+ if (direct)
3373
+ return direct;
3374
+ for (const [key, candidate] of Object.entries(map)) {
3375
+ if (String(key) === String(userId)) {
3376
+ const text = displayTextFromValue(candidate);
3377
+ if (text)
3378
+ return text;
3379
+ }
3380
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
3381
+ continue;
3382
+ const candidateIds = LOG_USER_ID_KEYS.flatMap((idKey) => valueToIdList(pickRawLoose(candidate, idKey)));
3383
+ if (candidateIds.some((id) => String(id) === String(userId))) {
3384
+ const text = displayTextFromValue(candidate);
3385
+ if (text)
3386
+ return text;
3387
+ }
3388
+ }
3389
+ return '';
3390
+ }
3391
+ /**
3392
+ * 收集 `userDataMap` 并建 `id → 用户对象` 索引
3393
+ * (来源:V11.3 `extract_user_data_map` / `normalize_user_data_map` / `add_user_map_item`)。
3394
+ *
3395
+ * 真机实测:`getRequestLog` 的 `userDataMap`(`[{UserName, userId}, …]`,90+ 条)挂在**每条日志项内部**
3396
+ * 而不是响应顶层 ⇒ 除了遍历响应,还要逐项收一遍,否则接收人只能显示裸 ID。
3397
+ */
3398
+ function collectUserDataMap(source) {
3399
+ const mapped = {};
3400
+ const addItem = (item, fallbackKey) => {
3401
+ if (fallbackKey !== undefined && fallbackKey !== '')
3402
+ mapped[String(fallbackKey)] = item;
3403
+ if (!item || typeof item !== 'object' || Array.isArray(item))
3404
+ return;
3405
+ const record = item;
3406
+ for (const key of LOG_USER_ID_KEYS) {
3407
+ const rawId = pickRawLoose(record, key);
3408
+ if (rawId !== null && rawId !== undefined && rawId !== '')
3409
+ mapped[String(rawId)] = item;
3410
+ }
3411
+ for (const nestedKey of ['user', 'userInfo', 'userBaseInfo', 'resource', 'resourceInfo', 'hrmResource', 'person', 'identity']) {
3412
+ const nested = record[nestedKey];
3413
+ if (nested && typeof nested === 'object' && !Array.isArray(nested))
3414
+ addItem(nested);
3415
+ }
3416
+ };
3417
+ const addMapLike = (value) => {
3418
+ if (Array.isArray(value)) {
3419
+ value.forEach((item) => addItem(item));
3420
+ return;
3421
+ }
3422
+ if (value && typeof value === 'object') {
3423
+ for (const [key, item] of Object.entries(value)) {
3424
+ if (Array.isArray(item))
3425
+ item.forEach((nested) => addItem(nested));
3426
+ else
3427
+ addItem(item, key);
3428
+ }
3429
+ }
3430
+ };
3431
+ const walk = (value, depth = 0) => {
3432
+ if (!value || typeof value !== 'object' || depth > 2)
3433
+ return;
3434
+ if (Array.isArray(value)) {
3435
+ value.forEach((item) => {
3436
+ if (item && typeof item === 'object') {
3437
+ const direct = pickRawLoose(item, 'userDataMap', 'userdataMap', 'userMap', 'userInfoMap', 'resourceDataMap');
3438
+ if (direct !== undefined)
3439
+ addMapLike(direct);
3440
+ }
3441
+ });
3442
+ return;
3443
+ }
3444
+ const record = value;
3445
+ const direct = pickRawLoose(record, 'userDataMap', 'userdataMap', 'userMap', 'userInfoMap', 'resourceDataMap');
3446
+ if (direct !== undefined)
3447
+ addMapLike(direct);
3448
+ for (const nestedKey of ['data', 'result', 'payload'])
3449
+ walk(record[nestedKey], depth + 1);
3450
+ };
3451
+ walk(source);
3452
+ return mapped;
3453
+ }
3454
+ /** 数字串 → 本地可读时间(`YYYY-MM-DD HH:mm:ss`)。 */
3455
+ function formatLogTimestamp(ms) {
3456
+ const date = new Date(ms);
3457
+ if (Number.isNaN(date.getTime()))
3458
+ return String(ms);
3459
+ const pad = (n) => String(n).padStart(2, '0');
3460
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} `
3461
+ + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
3462
+ }
3463
+ /**
3464
+ * 时间文本归一化(来源:V11.3 `normalize_log_time_text`)。
3465
+ *
3466
+ * 接口给的是**毫秒时间戳**(如 `1743082059000`)—— 不处理就会在签字意见里裸显一串数字。
3467
+ */
3468
+ function normalizeLogTimeText(value) {
3469
+ const clean = stripHtml(value);
3470
+ if (/^\d{13}$/u.test(clean))
3471
+ return formatLogTimestamp(Number(clean));
3472
+ if (/^\d{10}$/u.test(clean))
3473
+ return formatLogTimestamp(Number(clean) * 1000);
3474
+ return clean;
3475
+ }
3476
+ /** 去掉 `_weaverMte_` 系统标记(来源:V11.3 `strip_weaver_mte_tokens`)。 */
3477
+ function stripWeaverMteTokens(text) {
3478
+ if (!text)
3479
+ return text;
3480
+ return text.replace(/_weaverMte_\S+/gu, ' ').replace(/\s+/gu, ' ').trim();
3481
+ }
3482
+ /** 接收人(ID 列表 + 显示串)。来源:V11.3 `resolve_received_persons`。 */
3483
+ function resolveReceivedPersons(item, userDataMap) {
3484
+ const ids = valueToIdList(pickRawLoose(item, ...LOG_RECEIVER_ID_KEYS));
3485
+ let display = ids.map((id) => userNameFromMap(userDataMap, id) || id).join('、');
3486
+ if (!display)
3487
+ display = pickDisplayText(item, ...LOG_RECEIVER_TEXT_KEYS);
3488
+ return { ids, display };
3489
+ }
3490
+ /**
3491
+ * 递归收集日志条目(来源:V11.3 `collect_detail_logs_from_obj`)。
3492
+ *
3493
+ * ★ 关键在"递归":`data` 可能是数组、也可能是对象里某个键下面挂着数组。V12 早期只枚举四条固定路径,
3494
+ * 遇到 `getRequestLog` 的"`data` 是数组"形态就整条归零。
3495
+ */
3496
+ function collectDetailLogsFromObj(obj, logs, userDataMap, limit) {
3497
+ if (logs.length >= limit)
3498
+ return;
3499
+ if (Array.isArray(obj)) {
3500
+ for (const item of obj) {
3501
+ collectDetailLogsFromObj(item, logs, userDataMap, limit);
3502
+ if (logs.length >= limit)
3503
+ break;
3504
+ }
3505
+ return;
3506
+ }
3507
+ if (!obj || typeof obj !== 'object')
3508
+ return;
3509
+ const record = obj;
3510
+ const normalized = normalizeLogItem(record, userDataMap);
3511
+ if (detailLogHasContent(normalized))
3512
+ logs.push(normalized);
3513
+ for (const key of LOG_CONTAINER_KEYS) {
3514
+ if (key in record)
3515
+ collectDetailLogsFromObj(record[key], logs, userDataMap, limit);
3516
+ }
3517
+ }
3518
+ /**
3519
+ * 从接口响应里取出签字意见条目(来源:V11.3 `extract_request_log_items`)。
3520
+ *
3521
+ * 取 `response.data` 作为源(为空则回退整个 response),递归收集后按
3522
+ * `node/operator/time/action/receivers/opinion` 六元组去重。
3523
+ */
3524
+ function extractRequestLogItems(response, limit = 500) {
3525
+ const wrapper = response && typeof response === 'object' && !Array.isArray(response)
3526
+ ? response
3527
+ : {};
3528
+ let source = wrapper.data;
3529
+ const emptySource = source === null || source === undefined || source === ''
3530
+ || (Array.isArray(source) && source.length === 0)
3531
+ || (typeof source === 'object' && !Array.isArray(source) && Object.keys(source).length === 0);
3532
+ if (emptySource)
3533
+ source = response;
3534
+ const userDataMap = collectUserDataMap(response);
3535
+ const logs = [];
3536
+ collectDetailLogsFromObj(source, logs, userDataMap, limit);
3537
+ const seen = new Set();
3538
+ const unique = [];
3539
+ for (const log of logs) {
3540
+ const key = [log.node, log.operator, log.time, log.action, log.receivers, log.opinion]
3541
+ .map((part) => String(part ?? ''))
3542
+ .join('\u0001');
3543
+ if (seen.has(key))
3544
+ continue;
3545
+ seen.add(key);
3546
+ unique.push(log);
3547
+ }
3548
+ return unique;
3549
+ }
3550
+ /**
3551
+ * 详情响应里的签字意见(来源:V11.3 `extract_detail_logs`)。
3552
+ * 只认"键名像日志容器"的键,再走同一套递归收集 —— 不再枚举固定键名。
3553
+ */
3167
3554
  function extractRequestLogs(data) {
3168
- const logKeys = ['requestLogList', 'requestLogs', 'logs', 'logList', 'signList', 'records'];
3169
- const out = [];
3170
- for (const key of logKeys) {
3171
- const arr = toArray(data?.[key]);
3172
- if (arr.length)
3173
- out.push(...arr.map((l) => normalizeLogItem(l)).filter((l) => l && detailLogHasContent(l)));
3555
+ if (!data || typeof data !== 'object')
3556
+ return [];
3557
+ const userDataMap = collectUserDataMap(data);
3558
+ const logs = [];
3559
+ for (const [key, value] of Object.entries(data)) {
3560
+ if (!LOG_CONTAINER_KEY_PATTERN.test(key))
3561
+ continue;
3562
+ if (value === null || value === undefined || value === '')
3563
+ continue;
3564
+ if (Array.isArray(value) && value.length === 0)
3565
+ continue;
3566
+ collectDetailLogsFromObj(value, logs, userDataMap, 40);
3174
3567
  }
3175
- return out;
3568
+ return logs;
3176
3569
  }
3177
3570
  function detailLogHasContent(log) {
3178
- return Boolean(log && (log.operator || log.action || log.time || log.opinion || log.receivers));
3571
+ return Boolean(log && (log.node || log.operator || log.action || log.time || log.opinion || log.receivers));
3179
3572
  }
3180
3573
  // ---------------------------------------------------------------------------
3181
3574
  // 签字意见归一化(来源:normalize_detail_log_item / fetch_request_log_page)
3182
3575
  // ---------------------------------------------------------------------------
3576
+ /** 响应里的显式 hasMore 标志(来源:V11.3 `response_has_more_flag`)。 */
3577
+ function responseHasMoreFlag(response) {
3578
+ const wrapper = response && typeof response === 'object' && !Array.isArray(response)
3579
+ ? response
3580
+ : {};
3581
+ const data = wrapper.data && typeof wrapper.data === 'object' && !Array.isArray(wrapper.data)
3582
+ ? wrapper.data
3583
+ : {};
3584
+ const candidates = [];
3585
+ for (const key of ['hasMore', 'hasNext', 'more', 'next']) {
3586
+ if (key in wrapper)
3587
+ candidates.push(wrapper[key]);
3588
+ if (key in data)
3589
+ candidates.push(data[key]);
3590
+ }
3591
+ for (const value of candidates) {
3592
+ if (typeof value === 'boolean')
3593
+ return value;
3594
+ if (typeof value === 'string') {
3595
+ const lowered = value.trim().toLowerCase();
3596
+ if (['true', '1', 'yes'].includes(lowered))
3597
+ return true;
3598
+ if (['false', '0', 'no'].includes(lowered))
3599
+ return false;
3600
+ }
3601
+ }
3602
+ return null;
3603
+ }
3183
3604
  async function fetchRequestLogPage(env, requestId, current, pageSize) {
3184
3605
  const session = await env.host.getSession();
3185
- const payload = { requestId, current: Math.max(1, current), pageSize: Math.max(1, Math.min(50, pageSize)) };
3186
- if (session.userId)
3187
- payload.userId = session.userId;
3188
- const response = await env.host.workflowRequest({
3189
- method: 'POST',
3190
- path: WORKFLOW_ENDPOINTS.REQUEST_LOG,
3191
- operation: 'workflow.requestLog',
3192
- body: payload,
3193
- });
3194
- const data = responseData(response);
3195
- const items = toArray(data?.data?.list ?? data?.data?.records ?? data?.list ?? data?.records ?? data?.records ?? [])
3196
- .map((l) => normalizeLogItem(l))
3197
- .filter((l) => detailLogHasContent(l));
3606
+ const normalizedCurrent = Math.max(1, current || 1);
3607
+ const normalizedPageSize = Math.max(1, Math.min(50, pageSize || 10));
3608
+ const loadPage = async (page) => {
3609
+ const body = { requestId, current: page, pageSize: normalizedPageSize };
3610
+ if (session.userId)
3611
+ body.userId = session.userId;
3612
+ return env.host.workflowRequest({
3613
+ method: 'POST',
3614
+ path: WORKFLOW_ENDPOINTS.REQUEST_LOG,
3615
+ operation: 'workflow.requestLog',
3616
+ body,
3617
+ });
3618
+ };
3619
+ const response = await loadPage(normalizedCurrent);
3620
+ // ★ 取数按 V11.3 口径:把整个响应交给 `extractRequestLogItems`(取 `response.data` —— 实测是
3621
+ // **数组** —— 再递归收集)。V12 早期写死 `data.list`/`data.records` 四个路径,数组形态全部 miss。
3622
+ const items = extractRequestLogItems(response, normalizedPageSize * 2).slice(0, normalizedPageSize);
3198
3623
  const total = extractTotal(response);
3199
3624
  const totalReliable = total !== null;
3200
- const hasMore = asBool(data?.hasMore ?? data?.data?.hasMore, false) ||
3201
- (totalReliable ? current * pageSize < total : items.length >= pageSize);
3625
+ const hasMoreFlag = responseHasMoreFlag(response);
3626
+ let hasMore;
3627
+ if (hasMoreFlag !== null) {
3628
+ hasMore = hasMoreFlag;
3629
+ }
3630
+ else if (totalReliable) {
3631
+ hasMore = normalizedCurrent * normalizedPageSize < total;
3632
+ }
3633
+ else if (items.length < normalizedPageSize) {
3634
+ hasMore = false;
3635
+ }
3636
+ else {
3637
+ // 本页已满且接口没给 total ⇒ 探测下一页(来源:V11.3 `fetch_request_log_page` 的 next_page_probe)。
3638
+ // 直接假定 hasMore=true 会在最后一页误报"可继续查看第 N 页"。
3639
+ hasMore = extractRequestLogItems(await loadPage(normalizedCurrent + 1), 1).length > 0;
3640
+ }
3202
3641
  return {
3203
3642
  available: items.length > 0,
3204
- current,
3205
- pageSize,
3643
+ current: normalizedCurrent,
3644
+ pageSize: normalizedPageSize,
3206
3645
  currentPageCount: items.length,
3207
3646
  total,
3208
3647
  totalReliable,
3209
3648
  hasMore,
3210
- nextCurrent: hasMore ? current + 1 : null,
3211
- nextPagePrompt: hasMore ? `可继续查看第 ${current + 1} 页签字意见` : '',
3649
+ nextCurrent: hasMore ? normalizedCurrent + 1 : null,
3650
+ nextPagePrompt: hasMore ? `可继续查看第 ${normalizedCurrent + 1} 页签字意见` : '',
3212
3651
  items,
3213
- paginationText: `第 ${current} 页,每页 ${pageSize} 条`,
3652
+ paginationText: `第 ${normalizedCurrent} 页,每页 ${normalizedPageSize} 条`,
3214
3653
  paginationNote: '签字意见按页展示;只有 hasMore=true 时才提示查看下一页',
3215
3654
  };
3216
3655
  }
3217
- function normalizeLogItem(item) {
3656
+ /**
3657
+ * 签字意见单条归一化(来源:V11.3 `normalize_detail_log_item`)。
3658
+ *
3659
+ * 三处「V11.3 有、V12 缺」的处理,缺一个就会显示成空白或乱码:
3660
+ * ① **操作人取 `commentor`** —— 真机实测该字段是**对象**(`{id, name:'孙一一', username}`),
3661
+ * 必须走 `displayTextFromValue`;旧写法用 `pickText` 只会把对象 `String()` 成 `[object Object]`。
3662
+ * ② **时间走 `normalizeLogTimeText`** —— 接口给的是 **毫秒时间戳**(`1743082059000`)。
3663
+ * ③ **接收人走 `resolveReceivedPersons`** —— `receivedPersonIds`(id 串)+ `userDataMap`(id→姓名)解析;
3664
+ * 实测某条签字意见的接收人有 90+ 人,不解析就只能显示一串裸 ID。
3665
+ */
3666
+ function normalizeLogItem(item, userDataMap = {}) {
3218
3667
  if (!item || typeof item !== 'object')
3219
3668
  return {};
3220
- const operator = pickText(item, 'operatorName', 'operator', 'operatorUser', 'userName', 'handlerName', 'creatorName', 'handleUser');
3221
- const action = pickText(item, 'commentName', 'operateType', 'operateTypeName', 'operationType', 'actionName', 'typeName', 'status', 'statusName');
3222
- const time = pickText(item, 'operateTime', 'operateDateTime', 'handlerTime', 'createTime', 'createdatetime', 'createDateTime', 'time');
3223
- let opinion = pickText(item, 'opinion', 'remark', 'signRemark', 'content', 'comment', 'message');
3224
- const receivers = pickText(item, 'receiverName', 'receivers', 'receivePersonName', 'receiver');
3669
+ const mergedMap = { ...userDataMap, ...collectUserDataMap(item) };
3670
+ let node = pickDisplayText(item, ...LOG_NODE_KEYS);
3671
+ if (/^\d{12,}$/u.test(stripHtml(node)))
3672
+ node = '';
3673
+ const commentorText = displayTextFromValue(pickRawLoose(item, 'commentor', 'commentorId', 'commentorid', 'commentorInfo'));
3674
+ const operator = commentorText || pickDisplayText(item, ...LOG_OPERATOR_KEYS);
3675
+ const time = normalizeLogTimeText(pickDisplayText(item, ...LOG_TIME_KEYS));
3676
+ const action = pickDisplayText(item, ...LOG_ACTION_KEYS);
3677
+ const opinion = stripWeaverMteTokens(pickDisplayText(item, ...LOG_OPINION_KEYS));
3678
+ const receivers = resolveReceivedPersons(item, mergedMap);
3225
3679
  const operatorDisplay = operator || '未返回';
3226
3680
  const actionDisplay = action || '未返回';
3227
3681
  const timeDisplay = time || '未返回';
3228
3682
  const opinionDisplay = opinion || '未填写意见';
3229
3683
  return {
3684
+ node,
3230
3685
  operator,
3231
3686
  operatorDisplay,
3232
3687
  time,
3233
3688
  timeDisplay,
3234
3689
  action,
3235
3690
  actionDisplay,
3691
+ operatorAction: `${operatorDisplay} · ${actionDisplay}`,
3236
3692
  operatorActionTime: `${operatorDisplay} · ${actionDisplay} · ${timeDisplay}`,
3237
- receiverDisplay: receivers || '未返回',
3693
+ receiverIds: receivers.ids,
3694
+ receivers: receivers.display,
3695
+ receiverDisplay: receivers.display || '未返回',
3238
3696
  opinion,
3239
3697
  opinionDisplay,
3240
3698
  displayLines: {
3241
3699
  line1: `${operatorDisplay} · ${actionDisplay} · ${timeDisplay}`,
3242
- line2: `接收人:${receivers || '未返回'}`,
3700
+ line2: `接收人:${receivers.display || '未返回'}`,
3243
3701
  line3: opinionDisplay,
3244
3702
  },
3245
3703
  displayLayout: 'threeLineSignatureOpinion',
@@ -90,6 +90,29 @@ export function responseData(response) {
90
90
  return record.data;
91
91
  return response;
92
92
  }
93
+ /**
94
+ * 流程详情接口(`getWorkflowRequest`)响应里的**业务对象**。
95
+ *
96
+ * ★ 层级口径(2026-09-19 真机 fetch 探针实测,别再猜):真实响应是
97
+ * `{message, data:{workflowBaseInfo, otherInfo, formData}}` —— **只有一层 `data`**,
98
+ * 所以 `responseData()` 解掉外层之后**拿到的就是业务对象本身**。V11.3 同口径
99
+ * (`normalize_workflow_detail_response` 里 `response["data"]["workflowBaseInfo"]`,也只解一层)。
100
+ *
101
+ * 早期实现在 `responseData()` 之后**又取了一次 `.data`**(实际取到 `response.data.data`,不存在)
102
+ * ⇒ `workflow.detail` / `workflow.summary` 恒空(basicInfo 全空、`formData.available=false`),
103
+ * `workflow.write` 的授权门禁也因同源写法读不到 `mapItem`。
104
+ *
105
+ * 收口成本函数后两处共用,避免"一处改对、另一处忘了"(A.50)。
106
+ * 同时保留"某环境多包一层 `data`"的兜底,两种服务端实现都不会挂。
107
+ */
108
+ export function detailDataOf(response) {
109
+ const root = responseData(response);
110
+ if (!root || typeof root !== 'object')
111
+ return {};
112
+ if ('workflowBaseInfo' in root || 'otherInfo' in root || 'formData' in root)
113
+ return root;
114
+ return root.data && typeof root.data === 'object' ? root.data : {};
115
+ }
93
116
  /**
94
117
  * 记忆应用 ID(来源:原 Python `DEFAULT_MEMORY_APP_ID` / `memory_app_id_from_runtime`)。
95
118
  *
@@ -3,7 +3,7 @@ import { WorkflowCliError, workflowConfirmationError, workflowPartialError, work
3
3
  import { continuationReplayError } from '../continuation.js';
4
4
  import { rethrowAuthOrNetworkError } from './read.js';
5
5
  import { CONTINUATION_INPUT, SCHEMA_CATEGORY, SCHEMA_CONFIRM, SCHEMA_INDEX, SCHEMA_REQUEST_ID, schemaOf, workflowSuccess, } from './types.js';
6
- import { asInt, asString, asStringArray, compact, responseData, toArray, resolveMemoryAppId } from './shared.js';
6
+ import { asInt, asString, asStringArray, compact, responseData, detailDataOf, toArray, resolveMemoryAppId } from './shared.js';
7
7
  import { WORKFLOW_ENDPOINTS } from '../endpoints.js';
8
8
  import { findIndexedItem, loadSearchIndex, } from '../state.js';
9
9
  // ===========================================================================
@@ -195,9 +195,15 @@ async function resolveTargetViaDetail(env, requestId, action) {
195
195
  catch {
196
196
  throw workflowValidationError('request_detail_unavailable', `无法获取流程 ${requestId} 详情以校验操作权限,已停止操作(请通过列表序号操作)`, { requestId });
197
197
  }
198
- const data = responseData(detail)?.data;
199
- const mapItem = (data && typeof data === 'object' && data.mapItem && typeof data.mapItem === 'object' ? data.mapItem : undefined)
200
- ?? (data && typeof data === 'object' ? data : {});
198
+ // ★ 层级与 `normalizeDetailResponse` 同源:`detailDataOf` 已处理"`responseData()` 解过一层 `data`"
199
+ // (旧写法在这里又取一次 `.data` ⇒ 恒 undefined ⇒ 门禁永远拿不到 `mapItem`)。
200
+ // ⚠️ 但要清楚:该详情响应里**本来就没有** `mapItem` / `canSubmitRejectTransfer`(真机探针实测,
201
+ // 这两个字段只出现在列表 `displayData` 与会话映射里,`otherInfo` 常为空对象)⇒ 本函数在
202
+ // "直传 requestId"场景下通常仍拿不到门禁信息,`allowed` 为 undefined ⇒ **不拦截**。
203
+ // 这与 V11.3 的行为一致(`_normalized` 里没有 `mapItem` ⇒ 降级为 `{requestId}` ⇒ 同样不拦),
204
+ // 属于"缺少门禁数据",不是"误放行"——修正层级只是让 fallback 逻辑按其本意生效。
205
+ const data = detailDataOf(detail);
206
+ const mapItem = (data.mapItem && typeof data.mapItem === 'object' ? data.mapItem : undefined) ?? data;
201
207
  const isRemark = String(mapItem.isremark ?? mapItem.isRemark ?? '');
202
208
  const allowed = mapItem.canSubmitRejectTransfer;
203
209
  const isSpecialSubmit = ['agree', 'submit'].includes(String(action ?? '')) &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weaver-work-cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "面向泛微工作工具链的可扩展 TypeScript CLI 基础框架(E10 业票通等能力)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -77,6 +77,7 @@ function defaultOutput(skillNames) {
77
77
  function shouldSkip(entry, relativePath) {
78
78
  if (entry.name === '.DS_Store' || entry.name.startsWith('.')) return true
79
79
  if (relativePath === 'product.json') return true
80
+ if (relativePath === 'references/source-manifest.json') return true
80
81
  if (entry.isDirectory() && ['assets', 'node_modules', 'scripts'].includes(entry.name)) return true
81
82
  return false
82
83
  }
@@ -48,7 +48,7 @@ User-Agent: AgentType=<agentType>,IsAgent=true
48
48
  1. **第一轮就并行发起(关键:不要串行拆成多轮往返)**:在**同一条消息**里同时发出下列调用——
49
49
  - Read `../weaver-e10-shared/SKILL.md`(若尚未读过);
50
50
  - `weaver-work-cli --json workflow run workflow.memoryPrompt`——该命令**无入参**,命令形态已给定,**直接执行即可**(仅"非首次对话且距上次成功读取记忆不足 6 小时"时跳过);
51
- - **查询类意图**只读 [`workflow-query.md`](references/workflow-query.md) 一份(已覆盖 `workflow.search` 与 `workflow.todoStat`,不要再读其他查询文档);**但该文件约 105 KB / 821 行、一次读不完**——按它开头的**「分段索引」分次读,单次 `limit` 不超过 150 行**:**首轮并行读契约段两次**(`offset 1 limit 122` 与 `offset 123 limit 122`,已覆盖 Operation、全部输入参数含「大分类判定」、时间条件与「待办时间默认语义」、执行流程),其余章节(`planJson` 编排、汇报式分组骨架、渲染规则与示例)**只在需要的那一刻按索引给出的行区间补读**;查看/操作/批量/共享/发起/修改等意图读对应那一份 reference 即可,不要顺手把查询文档也一起读;
51
+ - **查询类意图**只读 [`workflow-query.md`](references/workflow-query.md) 一份(已覆盖 `workflow.search` 与 `workflow.todoStat`,不要再读其他查询文档);**但该文件约 105 KB / 820 行、一次读不完**——按它开头的**「分段索引」分次读,单次 `limit` 不超过 150 行**:**首轮并行读契约段两次**(`offset 1 limit 122` 与 `offset 123 limit 122`,已覆盖 Operation、全部输入参数含「大分类判定」、时间条件与「待办时间默认语义」、执行流程),其余章节(`planJson` 编排、汇报式分组骨架、渲染规则与示例)**只在需要的那一刻按索引给出的行区间补读**;查看/操作/批量/共享/发起/修改等意图读对应那一份 reference 即可,不要顺手把查询文档也一起读;
52
52
  - **泛化待办查询**(如"今天有哪些待办")直接 `workflow.search`(category=todo,**不要带 `planJson`/`subCategory`**)——**不要在这一轮先调 `workflow.todoStat`**:模式由 CLI 判定并在返回的 `renderMode` 里给出,未确认前就调 todoStat 在低版本必然失败(报 `todo_stat_requires_smart_mode`)白费一次调用;返回 `renderMode=legacy` 时 CLI 已自带分组统计与卡片,原样渲染即结束,`renderMode=smart` 时才追加 `workflow.todoStat` 做统计规划。
53
53
  > 以上调用**互不依赖,必须并行**:串行拆轮会成倍拉长耗时。**实测基准**:一次泛化待办查询全程约 70s,其中**模型轮次占 ~95%**(每轮约 8~25s,主因是模型输出量),CLI 命令本身合计只有 1~2s。**能省一轮就省一轮**——尤其不要为"解析上一轮输出""取流程 ID"另起一轮。
54
54
  - **[`workflow-memory.md`](references/workflow-memory.md) 本步不读**——记忆读取的命令形态已在上一行给出(直接执行 `workflow.memoryPrompt` 即可),**记忆保存的规则已在本文件「记忆保存(全局规则)」章节给全**(本轮任务结束时按那一节执行);只有需要判断"放 `user.md` / `memory.md` / `workflow:{id}` 哪一类"或其它字段细节时,才在需要的那一刻去读它。
@@ -9,7 +9,7 @@
9
9
  "kind": "workbuddy-script-skill",
10
10
  "availability": "local-installed-skill"
11
11
  },
12
- "generatedAt": "2026-09-19T01:43:47Z",
12
+ "generatedAt": "2026-09-19T08:58:27Z",
13
13
  "sourceSet": {
14
14
  "aggregateSha256": "5bf1b914f284a8da850aab749bdee077070d8591f9a9fb658e5b10c5db923bbb",
15
15
  "sourceRoots": [
@@ -355,7 +355,6 @@
355
355
  "tabid",
356
356
  "tabName",
357
357
  "subCategory",
358
- "keyword",
359
358
  "creator",
360
359
  "creatorDept",
361
360
  "creatorSubcom",
@@ -925,7 +924,7 @@
925
924
  }
926
925
  },
927
926
  "artifactBaseline": {
928
- "aggregateSha256": "132371c2cb2a0fdd55d05bc20f74078f52ad845c759893167b454f2731e618dd",
927
+ "aggregateSha256": "9c49b24b03fc62f5355d942a911b7ed6b0c5983e9debbdaa87c22c8afe22247d",
929
928
  "files": [
930
929
  {
931
930
  "path": "skills/weaver-e10-workflow/references/workflow-agent-entry.md",
@@ -939,8 +938,8 @@
939
938
  },
940
939
  {
941
940
  "path": "skills/weaver-e10-workflow/references/workflow-create.md",
942
- "size": 23807,
943
- "sha256": "43dd2ac24dd52808e378a314936bce7febf61604c0e7a555505e39336794c66f"
941
+ "size": 24620,
942
+ "sha256": "58a88c9a6d13cc008d035727609bb5e6f6af48812d971eeaf8665695876b8efe"
944
943
  },
945
944
  {
946
945
  "path": "skills/weaver-e10-workflow/references/workflow-edit.md",
@@ -959,8 +958,8 @@
959
958
  },
960
959
  {
961
960
  "path": "skills/weaver-e10-workflow/references/workflow-query.md",
962
- "size": 105456,
963
- "sha256": "7717a8c5978874598cc89bf6cc3813d06a5b0402c0e4937218b0e9c887f02160"
961
+ "size": 105418,
962
+ "sha256": "675586b364b16a45864cfeb4318da5399e82cfa549127aa0c6a76d58a113b0b9"
964
963
  },
965
964
  {
966
965
  "path": "skills/weaver-e10-workflow/references/workflow-share.md",
@@ -969,13 +968,13 @@
969
968
  },
970
969
  {
971
970
  "path": "skills/weaver-e10-workflow/references/workflow-view.md",
972
- "size": 10275,
973
- "sha256": "e2b3a1651706919d0a64dfe7b3861f0accbc6a5aea893eebfa299b52832fea1d"
971
+ "size": 10834,
972
+ "sha256": "a819dec23a20dd2f9a4fb03a13a0aeb74e7f8815e968fd4e76917c126aadaa68"
974
973
  },
975
974
  {
976
975
  "path": "skills/weaver-e10-workflow/SKILL.md",
977
976
  "size": 35688,
978
- "sha256": "ecfc4dd67a5389734698eca85d57ab688a0fb0e364db7e2ac5b35f539470aca8"
977
+ "sha256": "01ae14c997bfef2919db1f292314e3015de624217c062383f3c2e00fed436a32"
979
978
  },
980
979
  {
981
980
  "path": "src/shortcuts/workflow/continuation.ts",
@@ -1014,13 +1013,13 @@
1014
1013
  },
1015
1014
  {
1016
1015
  "path": "src/shortcuts/workflow/operations/form.ts",
1017
- "size": 191256,
1018
- "sha256": "2a7ee56ce0e8bc189d11ecd7bbe557769fe7c4489f182550abadb7210d0fc203"
1016
+ "size": 194756,
1017
+ "sha256": "40d0dda7b264bb9caa7022745e1fb467feee8a966a58797f94730caa74cbeb5b"
1019
1018
  },
1020
1019
  {
1021
1020
  "path": "src/shortcuts/workflow/operations/read.ts",
1022
- "size": 194246,
1023
- "sha256": "2a5045522214f14cb32b741bb93ebff1078f483729c2fc8f1d954400ad503581"
1021
+ "size": 215109,
1022
+ "sha256": "293c826b7d8f554f1945a9752e19e35f6fc59a91c267465b160aec92643ccafb"
1024
1023
  },
1025
1024
  {
1026
1025
  "path": "src/shortcuts/workflow/operations/registry.ts",
@@ -1029,8 +1028,8 @@
1029
1028
  },
1030
1029
  {
1031
1030
  "path": "src/shortcuts/workflow/operations/shared.ts",
1032
- "size": 6886,
1033
- "sha256": "5496922cb1807476f27b9da626b57b39fea020a4a267c70c2ae368e76a184544"
1031
+ "size": 8206,
1032
+ "sha256": "7517b587bd6c08d6a0ad6003a0fdd786e36e0eebb3beef922ab2650c5d9612a7"
1034
1033
  },
1035
1034
  {
1036
1035
  "path": "src/shortcuts/workflow/operations/types.ts",
@@ -1039,8 +1038,8 @@
1039
1038
  },
1040
1039
  {
1041
1040
  "path": "src/shortcuts/workflow/operations/write.ts",
1042
- "size": 102936,
1043
- "sha256": "bce32b59b3df196d5e65a8df59df719e0ba452294d2881a4c05a0a398df604f1"
1041
+ "size": 103697,
1042
+ "sha256": "78175dfa0168477efe3f3ebd8f1bad305d4f556e66c11724622bc2ce666ab2ee"
1044
1043
  },
1045
1044
  {
1046
1045
  "path": "src/shortcuts/workflow/state.ts",
@@ -8,7 +8,7 @@
8
8
 
9
9
  | Operation | 风险 | 固定规则 |
10
10
  | --- | --- | --- |
11
- | `workflow.createList` | read | 查询可发起工作流列表;不传凭据;`workflowId` 与 `name` 可同传精确校验 |
11
+ | `workflow.createList` | read | 查询可发起工作流列表;不传凭据;`name`/`workflowId` **至少传一个**;`workflowId` 与 `name` 可同传精确校验 |
12
12
  | `workflow.createForm` | read | 必传 `workflowId`;`relatesTo` 仅透传不影响表单初始化;返回 `context`/runtime/frame/字段清单/fileUploadParams |
13
13
  | `workflow.createPrepare` | read-before-write | requiresConfirmation;必传 `context`/`data`/`intent`;`requestLevel` 取 `systemFields.levelOptions` 的 code,`secLevel` 取 `systemFields.secLevelOptions` 的 id;不落业务数据 |
14
14
  | `workflow.createApply` | high-risk-write | requiresConfirmation;必传 `confirm:true`/`context`/`intent`;`intent` 不继承 prepare;遇 `partial`/`write_uncertain` 禁止重试 |
@@ -33,7 +33,7 @@
33
33
 
34
34
  ### workflow.createList
35
35
 
36
- - `name`(string):工作流名称关键字,模糊匹配。
36
+ - `name`(string):工作流名称关键字,模糊匹配。**与 `workflowId` 至少传一个**——两个都不传时 CLI **不做查询**(不发列表请求),直接返回 `MISSING_QUERY` 和提示。
37
37
  - `workflowId`(string,可选):记忆中已确认的工作流 ID。记忆有 ID 时名称与 ID 同传,按当前用户可发起权限列表精确校验并优先选中;命中 `SELECTED`,未命中 `NOT_FOUND`,禁止去掉 ID 退回名称候选。
38
38
 
39
39
  **查询词策略(第 ① 步必读)**:
@@ -138,7 +138,7 @@ weaver-work-cli --json workflow run workflow.createApply --input-json '{"context
138
138
 
139
139
  ## 返回
140
140
 
141
- - createList:`SELECTED`/`NEEDS_SELECTION`/`NOT_FOUND`;候选工作流名原样展示,不加"推荐"后缀。
141
+ - createList:`SELECTED`/`NEEDS_SELECTION`/`NOT_FOUND`/`MISSING_QUERY`;候选工作流名原样展示,不加"推荐"后缀。`MISSING_QUERY` = `name` 与 `workflowId` 都没传,CLI **未发起查询**、`workflows` 为空 —— 补上名称或 ID 后重调即可(**不要试图拉全量再自己筛**)。**大清单形态**(`name` 命中太多、清单超 24 KB 时):结果最前面多出 `resultFile` / `persistedResult` / `outputBytes` / `workflowsTotal` / `workflowsTruncated` / `largeOutputNotice`,且 `workflows` 只保留**前 100 条**——需要更多候选时从 `resultFile` 按需提取(**不要整份读入上下文**),更推荐把 `name` 写得更精确、或直接传 `workflowId` 来定位。
142
142
  - createForm:`context`、`runtime`、`frame`、`requiredFields`(字段 id 数组)、`subFormRequired`(subFormId → 字段 id 数组)、`editableFields`(**可写字段 id 白名单**)、`fields`(大表单时可能换成紧凑编码、或被整体移出 stdout —— 移出时同伴有 `writableFields`)、`initialValues`、`systemFields`、`fileUploadParams`、`requiredInfoWarning`、`conditionalRequired`(存在条件必填规则时才返回,见「注意」;**输出过大时它与 `editableFields` 一起移出 stdout、只留 `conditionalRequiredCount` 条数**,需要时按 `resultFile` 取)。
143
143
  - createPrepare:确认预览(`READY` 时在 `fields`)、`previewDetails`(明细表按列组织:`columns` 表头 + `rows` 每行明细,**仅 `READY` 时返回**;明细表按它排 Excel 列表格)、`resolvedValues`(`NEEDS_INPUT`/`NEEDS_CONFIRMATION` 时的解析回显)、`missing_fields`、`linkageResult`(联动带出/清空,原样转述)、`attachmentFolderIds`。
144
144
  - createApply:`finalMarkdown`、`requestId`、`requestMark`、标题、`url`;`finalMarkdown` 直接原样输出。`requestMark` 是用户可见流程编号(`finalMarkdown` 已按接口返回渲染:有值显示、无值不显示),`requestId` 只是系统内部流程请求 ID。**明细表展示**:`finalMarkdown` 中明细表已按 Excel 列展示(表头 + 每行明细),直接原样输出。
@@ -1,18 +1,18 @@
1
1
  # 流程查询(workflow.search / workflow.todoStat)
2
2
 
3
- > **分段索引 —— 本文件约 105 KB / 821 行,必须按段读:单次 `limit` 不超过 150 行,不要整读。**
3
+ > **分段索引 —— 本文件约 105 KB / 820 行,必须按段读:单次 `limit` 不超过 150 行,不要整读。**
4
4
  > 单次读取过大会被宿主落盘、只回一小段预览,而那个落盘文件**不可读**(去读它只会拿到 `Permission … has been denied`,白费一轮);读不全时回到本文件按行段重读即可。
5
5
  >
6
6
  > | 段落 | 行区间 | 读取方式 |
7
7
  > | --- | --- | --- |
8
- > | **契约段**:本索引 + Operation + 输入参数(分类 + 大分类判定 / 筛选 / 时间 / filterFields / 分页排序)+ 执行模式与返回形态 + 待办时间默认语义 + 命令参数反思 + 执行流程 | 1-244 | **首轮并行读两次**:`offset 1 limit 122`、`offset 123 limit 122` |
9
- > | `planJson` 批量编排 | 245-324 | 泛化待办 / 一次取多组数时读:`offset 245 limit 80` |
10
- > | 汇报式分组骨架 + 七个维度 + 参与身份与分组去向 + 常见业务重点字段 + 汇总渲染规则 + 渲染红线 | 325-507 | 需要输出分组结果前读(分两次):`offset 325 limit 100`、`offset 425 limit 83` |
11
- > | 智能分组示例(各业务域样例,**仅供格式参照,不要照抄内容**) | 508-622 | 一般**不用读** |
12
- > | **异常返回处理 + 澄清后回传**(澄清信封、修正指令、认证失效) | 623-688 | 拿到 `ok:false` 或 `NEEDS_CLARIFICATION` 时读:`offset 623 limit 66` |
13
- > | 调用示例 | 689-767 | 需要时读:`offset 689 limit 79` |
14
- > | 返回(返回结构 / 错误码 / 幂等口径) | 768-807 | 处理 `ok` / `error` 时读:`offset 768 limit 40` |
15
- > | 注意 | 808-821 | 需要时读:`offset 808 limit 14` |
8
+ > | **契约段**:本索引 + Operation + 输入参数(分类 + 大分类判定 / 筛选 / 时间 / filterFields / 分页排序)+ 执行模式与返回形态 + 待办时间默认语义 + 命令参数反思 + 执行流程 | 1-243 | **首轮并行读两次**:`offset 1 limit 122`、`offset 123 limit 122`(合计覆盖 1-244,含本段全部) |
9
+ > | `planJson` 批量编排 | 244-323 | 泛化待办 / 一次取多组数时读:`offset 244 limit 80` |
10
+ > | 汇报式分组骨架 + 七个维度 + 参与身份与分组去向 + 常见业务重点字段 + 汇总渲染规则 + 渲染红线 | 324-506 | 需要输出分组结果前读(分两次):`offset 324 limit 100`、`offset 424 limit 83` |
11
+ > | 智能分组示例(各业务域样例,**仅供格式参照,不要照抄内容**) | 507-621 | 一般**不用读** |
12
+ > | **异常返回处理 + 澄清后回传**(澄清信封、修正指令、认证失效) | 622-687 | 拿到 `ok:false` 或 `NEEDS_CLARIFICATION` 时读:`offset 622 limit 66` |
13
+ > | 调用示例 | 688-766 | 需要时读:`offset 688 limit 79` |
14
+ > | 返回(返回结构 / 错误码 / 幂等口径) | 767-806 | 处理 `ok` / `error` 时读:`offset 767 limit 40` |
15
+ > | 注意 | 807-820 | 需要时读:`offset 807 limit 14` |
16
16
  >
17
17
  > 行号以本索引为准;**改本文件时同步更新本索引**。按 `##` 标题定位同样有效。
18
18
 
@@ -96,7 +96,6 @@
96
96
  | 当前节点 ID | `nodeId`、`nodeIdTerm`(`in`/`notin`) | 节点 ID |
97
97
  | 流程状态 | `flowStatus`、`flowStatusTerm`(`in`/`notin`) | 状态值;`已结束` = 强制+正常结束 |
98
98
  | 精确过滤/排除 | `requestId`、`exRequestId` | 逗号分隔多个 requestId |
99
- | 实体关键词 | `keyword`(数组) | 辅助解析用的实体关键词 |
100
99
  | 排序 | `sort` | `"排序方式:asc\|desc"`,多组逗号分隔 |
101
100
  | 条件关系 | `conditionType` | `and`(默认)/`or` |
102
101
 
@@ -184,6 +184,7 @@ weaver-work-cli --json workflow run workflow.pageOpen --input-json '{"page":"vie
184
184
  - **摘要不标记已读**:`workflow.summary` 不改变流程未读状态;`workflow.detail` 会标记已读(用户主动查看语义),按需选择。
185
185
  - **定位纪律**:用户已给 `requestId` 时不要先查列表再过滤;分组场景必须 `group` + `index` 同时传;智能分组模式只传 `index`。
186
186
  - **大结果**:详情签字意见按 `requestLogPageSize` 分页,仅 `hasMore=true` 提示下一页;需要原始 JSON 时落本地文件返回路径。
187
+ - **`warnings`(取不到详情时必须如实说)**:`workflow.detail` 在**接口返回成功但什么都没取到**(流程标题/编号/发起人/当前节点/表单字段全为空)时,会在 `warnings` 里给出原因提示。出现 `warnings` 时**必须原样转述给用户**——不要静默忽略,也不要把空详情当成"这个流程没有内容"就照常渲染空白面板;先核对 `requestId` 是否取自本环境查询结果的 `cards[].requestId`,确实取不到就如实说明详情不可见,不要编造流程内容。
187
188
  - **失败/停止**:CLI 失败时在 **stderr** 返回 `ok:false` + `error.type`/`error.subtype`/`error.message`/`error.retryable`(**没有** `success`/`stopReason`/`userMessage` 字段)——`retryable=true` 时最多重试一次,否则原样转述 `error.message` 并停止,不重试、不换接口。入参类错误(如 `locate_required`)按 `error.message` 补齐参数后重试一次。
188
189
  - **未登录/登录失效**:按 `weaver-e10-shared` 的 `references/e10-auth-and-session.md` 引导用户登录,登录后重放原请求(读操作可重放)。
189
190
  - **凭据禁止**:业务输入里禁止传入 Cookie、ETEAMSID、Token、`header.operator`,这些由 CLI 托管。
@@ -28,7 +28,7 @@
28
28
  - 企业专票或需要企业抬头时:必须由用户或附件提供购方名称、税号;地址、电话、开户行和账号有明确来源时才填入,没有来源不要编造。
29
29
  - 从附件提取购方信息前,必须先提醒用户文件内容可能进入当前大模型上下文,并可能发送到解析服务,等待明确确认后再处理。
30
30
 
31
- CLI 会固定销方名称 `上海泛微网络科技股份有限公司`、销方税号 `9131000070322836XD`,并补齐 `billingType="0"`、`sourceType="4"`、`asyncCallFlag="1"`、`invoiceMethod="5"`、`taxSign="1"`。Agent 不要把 Cookie、ETEAMSID、业务 Token 或认证字段放入 payload。
31
+ 销方为 CLI 内置固定值(当前版本固定为销方名称 `上海泛微网络科技股份有限公司`、销方税号 `9131000070322836XD`,非按当前 E10 租户动态解析),Agent 无需也不应自行构造或改写销方字段;CLI 会一并补齐 `billingType="0"`、`sourceType="4"`、`asyncCallFlag="1"`、`invoiceMethod="5"`、`taxSign="1"`。Agent 不要把 Cookie、ETEAMSID、业务 Token 或认证字段放入 payload。
32
32
 
33
33
  ## 命令
34
34
 
@@ -37,13 +37,13 @@ CLI 会固定销方名称 `上海泛微网络科技股份有限公司`、销方
37
37
  Windows PowerShell:
38
38
 
39
39
  ```powershell
40
- weaver-work-cli --json invoice run invoice.issuing.make.prepare --input-json '{"mode":"ledger","payload":{"invoiceType":"8","externalDocumentNo":"AI-20260827-001","purchaserName":"XX科技有限公司","purchaserTaxNo":"91310000MA1234AB56","totalAmount":100,"taxAmount":6,"totalAmountWithTax":106,"details":[{"goodsName":"信息技术咨询服务","revenueCode":"3040205","taxRate":0.06,"amount":100,"amountWithTax":106,"taxAmount":6,"quantity":"1","unit":"项"}]}}'
40
+ weaver-work-cli --json invoice run invoice.issuing.make.prepare --input-json '{"mode":"ledger","payload":{"invoiceType":"8","externalDocumentNo":"AI-EXAMPLE-001","purchaserName":"XX科技有限公司","purchaserTaxNo":"91310000MA1234AB56","totalAmount":100,"taxAmount":6,"totalAmountWithTax":106,"details":[{"goodsName":"信息技术咨询服务","revenueCode":"3040205","taxRate":0.06,"amount":100,"amountWithTax":106,"taxAmount":6,"quantity":"1","unit":"项"}]}}'
41
41
  ```
42
42
 
43
43
  macOS/Linux(bash/zsh):
44
44
 
45
45
  ```bash
46
- weaver-work-cli --json invoice run invoice.issuing.make.prepare --input-json '{"mode":"ledger","payload":{"invoiceType":"8","externalDocumentNo":"AI-20260827-001","purchaserName":"XX科技有限公司","purchaserTaxNo":"91310000MA1234AB56","totalAmount":100,"taxAmount":6,"totalAmountWithTax":106,"details":[{"goodsName":"信息技术咨询服务","revenueCode":"3040205","taxRate":0.06,"amount":100,"amountWithTax":106,"taxAmount":6,"quantity":"1","unit":"项"}]}}'
46
+ weaver-work-cli --json invoice run invoice.issuing.make.prepare --input-json '{"mode":"ledger","payload":{"invoiceType":"8","externalDocumentNo":"AI-EXAMPLE-001","purchaserName":"XX科技有限公司","purchaserTaxNo":"91310000MA1234AB56","totalAmount":100,"taxAmount":6,"totalAmountWithTax":106,"details":[{"goodsName":"信息技术咨询服务","revenueCode":"3040205","taxRate":0.06,"amount":100,"amountWithTax":106,"taxAmount":6,"quantity":"1","unit":"项"}]}}'
47
47
  ```
48
48
 
49
49
  复杂 payload 建议保存为 UTF-8 JSON 文件。
@@ -56,7 +56,7 @@ Set-Content -Encoding utf8 -LiteralPath .\issuing.json -Value @'
56
56
  "mode": "issue",
57
57
  "payload": {
58
58
  "invoiceType": "8",
59
- "externalDocumentNo": "AI-20260827-001",
59
+ "externalDocumentNo": "AI-EXAMPLE-001",
60
60
  "purchaserName": "XX科技有限公司",
61
61
  "purchaserTaxNo": "91310000MA1234AB56",
62
62
  "totalAmount": 100,
@@ -77,7 +77,7 @@ cat > ./issuing.json <<'JSON'
77
77
  "mode": "issue",
78
78
  "payload": {
79
79
  "invoiceType": "8",
80
- "externalDocumentNo": "AI-20260827-001",
80
+ "externalDocumentNo": "AI-EXAMPLE-001",
81
81
  "purchaserName": "XX科技有限公司",
82
82
  "purchaserTaxNo": "91310000MA1234AB56",
83
83
  "totalAmount": 100,
@@ -194,7 +194,7 @@ CLI 信封中列表在 `data.items`。下面是单条发票的票面结构(对
194
194
  },
195
195
  "buyer": {
196
196
  "tcode": "9131000070322836XD",
197
- "company": "泛微网络科技股份有限公司"
197
+ "company": "上海泛微网络科技股份有限公司"
198
198
  },
199
199
  "payer": {
200
200
  "tcode": "91460200348073438U",