weaver-work-cli 0.1.26 → 0.1.29

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 (163) hide show
  1. package/README.md +7 -2
  2. package/dist/cmd/setup/index.js +1 -1
  3. package/dist/internal/e10/auth/commands.js +1 -6
  4. package/dist/internal/e10/auth/messages.js +3 -0
  5. package/dist/internal/e10/auth/session.js +8 -0
  6. package/dist/internal/e10/auth-recovery.js +148 -0
  7. package/dist/internal/e10/client.js +47 -15
  8. package/dist/internal/e10/context.js +8 -8
  9. package/dist/internal/e10/index.js +1 -0
  10. package/dist/internal/e10/request-runtime.js +51 -9
  11. package/dist/shortcuts/archive/host.js +3 -2
  12. package/dist/shortcuts/archive/manifest.js +1 -1
  13. package/dist/shortcuts/archive-oa/host.js +2 -1
  14. package/dist/shortcuts/archive-oa/manifest.js +1 -1
  15. package/dist/shortcuts/asset/host.js +7 -6
  16. package/dist/shortcuts/blog/host.js +4 -3
  17. package/dist/shortcuts/caizhilian/host.js +3 -2
  18. package/dist/shortcuts/caizhilian/manifest.js +1 -1
  19. package/dist/shortcuts/calendar/host.js +2 -1
  20. package/dist/shortcuts/contract/host.js +34 -59
  21. package/dist/shortcuts/ehr/host.js +2 -1
  22. package/dist/shortcuts/esb/host.js +2 -1
  23. package/dist/shortcuts/fna/host.js +6 -3
  24. package/dist/shortcuts/fna/manifest.js +10 -5
  25. package/dist/shortcuts/fna/operations/expense-chart.js +162 -0
  26. package/dist/shortcuts/fna/operations/registry.js +2 -0
  27. package/dist/shortcuts/hrm/host.js +2 -1
  28. package/dist/shortcuts/im/host.js +3 -2
  29. package/dist/shortcuts/invoice/host.js +9 -7
  30. package/dist/shortcuts/invoice/manifest.js +23 -142
  31. package/dist/shortcuts/invoice/operations/issuing-qys.js +266 -203
  32. package/dist/shortcuts/invoice/operations/registry.js +1 -6
  33. package/dist/shortcuts/jiuchuanhui/host.js +2 -1
  34. package/dist/shortcuts/mail/host.js +2 -1
  35. package/dist/shortcuts/meeting/host.js +2 -1
  36. package/dist/shortcuts/meeting/operations/eb/conflict.js +78 -12
  37. package/dist/shortcuts/okr/host.js +2 -1
  38. package/dist/shortcuts/plan/host.js +2 -1
  39. package/dist/shortcuts/project/host.js +2 -1
  40. package/dist/shortcuts/workflow/host.js +3 -5
  41. package/dist/shortcuts/workflow/index.js +4 -1
  42. package/dist/shortcuts/workflow/operations/chatscene.js +4 -22
  43. package/dist/shortcuts/workflow/operations/form.js +281 -166
  44. package/dist/shortcuts/workflow/operations/read.js +39 -8
  45. package/dist/shortcuts/workflow/operations/registry.js +2 -0
  46. package/dist/shortcuts/workflow/operations/todoSmartConvert/businessRules.js +331 -0
  47. package/dist/shortcuts/workflow/operations/todoSmartConvert/detailFieldRules.js +133 -0
  48. package/dist/shortcuts/workflow/operations/todoSmartConvert/summaryCache.js +326 -0
  49. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailCache.js +113 -0
  50. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoDetailFetcher.js +259 -0
  51. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoListRows.js +64 -0
  52. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoOutputPlanner.js +28 -0
  53. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.aggregate.js +70 -0
  54. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.collect.js +267 -0
  55. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.core.js +392 -0
  56. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.detail.js +165 -0
  57. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.group.js +448 -0
  58. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.groupSummary.js +348 -0
  59. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.js +222 -0
  60. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.pipeline.js +66 -0
  61. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.smartGroup.js +502 -0
  62. package/dist/shortcuts/workflow/operations/todoSmartConvert/todoSmartConvert.summary.js +540 -0
  63. package/dist/shortcuts/workflow/stdout-compact.js +7 -0
  64. package/docs/SKILL.md +1 -1
  65. package/docs/_catalog.md +2 -1
  66. package/docs/agent-invoice.md +1 -1
  67. package/docs/caizhilian.md +1 -1
  68. package/docs/e10-auth.md +1 -1
  69. package/docs/fna.md +7 -3
  70. package/docs/invoice.md +4 -6
  71. package/docs/todo-smart-convert/README.md +171 -0
  72. package/docs/todo-smart-convert/data-contract.md +236 -0
  73. package/docs/todo-smart-convert/mock/ai-todo-query-target.json +209 -0
  74. package/docs/todo-smart-convert/subgroup-rules.md +245 -0
  75. package/package.json +1 -1
  76. package/scripts/package-skill.mjs +1 -1
  77. package/skill-template/business-info.json +4 -1
  78. package/skill-template/domains/caizhilian.md +2 -1
  79. package/skill-template/domains/qiyecheng.md +1 -0
  80. package/skill-template/domains/yepiaotong.md +6 -2
  81. package/skill-template/skill-template.md +1 -0
  82. package/skills/weaver-e10-blog/SKILL.md +6 -1
  83. package/skills/weaver-e10-blog/references/blog-query.md +3 -1
  84. package/skills/weaver-e10-caizhilian/SKILL.md +18 -9
  85. package/skills/weaver-e10-caizhilian/references/action-flow.md +70 -0
  86. package/skills/weaver-e10-caizhilian/references/agent-entry.md +6 -2
  87. package/skills/weaver-e10-caizhilian/references/auth-and-install.md +207 -0
  88. package/skills/weaver-e10-caizhilian/references/file-collection.md +56 -54
  89. package/skills/weaver-e10-caizhilian/references/file-upload.md +4 -4
  90. package/skills/weaver-e10-caizhilian/references/host-executor-prompt.md +32 -0
  91. package/skills/weaver-e10-caizhilian/references/host-platform-integration.md +46 -0
  92. package/skills/weaver-e10-caizhilian/references/rule-index.md +56 -0
  93. package/skills/weaver-e10-caizhilian/references/setting-attach.md +5 -16
  94. package/skills/weaver-e10-caizhilian/references/setting-backup.md +4 -13
  95. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +6 -15
  96. package/skills/weaver-e10-caizhilian/references/setting-storage-docs-summary.md +126 -0
  97. package/skills/weaver-e10-caizhilian/references/setting-storage-field-map.md +57 -0
  98. package/skills/weaver-e10-caizhilian/references/setting-storage.md +142 -482
  99. package/skills/weaver-e10-caizhilian/references/source-manifest.json +7 -7
  100. package/skills/weaver-e10-caizhilian/references/task-workflow.md +87 -0
  101. package/skills/weaver-e10-caizhilian/references/withheld.md +2 -2
  102. package/skills/weaver-e10-caizhilian/references/write-protocol.md +7 -0
  103. package/skills/weaver-e10-calendar/SKILL.md +6 -1
  104. package/skills/weaver-e10-esb/SKILL.md +8 -0
  105. package/skills/weaver-e10-esb/references/input-format.md +3 -1
  106. package/skills/weaver-e10-esb/references/source-manifest.json +5 -5
  107. package/skills/weaver-e10-hrm/SKILL.md +6 -1
  108. package/skills/weaver-e10-hrm/references/employee.md +1 -1
  109. package/skills/weaver-e10-hrm/references/org.md +1 -1
  110. package/skills/weaver-e10-hrm/references/position.md +1 -1
  111. package/skills/weaver-e10-jinchengda/SKILL.md +2 -1
  112. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +1 -1
  113. package/skills/weaver-e10-jiuchuanhui/SKILL.md +6 -1
  114. package/skills/weaver-e10-jucailin/SKILL.md +6 -1
  115. package/skills/weaver-e10-jucailin/references/hr-contact.md +1 -1
  116. package/skills/weaver-e10-jucailin/references/salary.md +1 -1
  117. package/skills/weaver-e10-mail/SKILL.md +6 -1
  118. package/skills/weaver-e10-meeting/SKILL.md +11 -6
  119. package/skills/weaver-e10-meeting/references/meeting-eb.md +7 -3
  120. package/skills/weaver-e10-meeting/references/meeting-queries.md +3 -1
  121. package/skills/weaver-e10-meeting/references/meeting-verification.md +4 -2
  122. package/skills/weaver-e10-okr/SKILL.md +6 -1
  123. package/skills/weaver-e10-okr/references/source-manifest.json +5 -5
  124. package/skills/weaver-e10-plan/SKILL.md +6 -1
  125. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +3 -1
  126. package/skills/weaver-e10-plan/references/plan-report-query.md +3 -1
  127. package/skills/weaver-e10-plan/references/source-manifest.json +7 -7
  128. package/skills/weaver-e10-qiyecheng/SKILL.md +16 -6
  129. package/skills/weaver-e10-qiyecheng/references/catalog.md +6 -3
  130. package/skills/weaver-e10-qiyecheng/references/expense-center.md +2 -0
  131. package/skills/weaver-e10-qiyecheng/references/expense-chart.md +78 -0
  132. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +87 -33
  133. package/skills/weaver-e10-shared/SKILL.md +37 -14
  134. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +11 -8
  135. package/skills/weaver-e10-shared/references/non-interactive-environment.md +4 -4
  136. package/skills/weaver-e10-shared/references/weaver-e10-installation.md +3 -1
  137. package/skills/weaver-e10-wenshuding/SKILL.md +6 -1
  138. package/skills/weaver-e10-wenshuding/references/source-manifest.json +3 -3
  139. package/skills/weaver-e10-workflow/SKILL.md +23 -1
  140. package/skills/weaver-e10-workflow/references/source-manifest.json +1 -2
  141. package/skills/weaver-e10-workflow/references/todo-smart-convert.md +282 -0
  142. package/skills/weaver-e10-workflow/references/workflow-create.md +30 -55
  143. package/skills/weaver-e10-workflow/scripts/todo-summary.cjs +695 -0
  144. package/skills/weaver-e10-yepiaotong/SKILL.md +19 -17
  145. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +75 -78
  146. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-form-fill-rules.md +179 -0
  147. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-workflow.md +238 -0
  148. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +7 -7
  149. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +11 -5
  150. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +55 -33
  151. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +102 -23
  152. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +1 -1
  153. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +53 -42
  154. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +94 -214
  155. package/skills/weaver-e10-yimiaoban/SKILL.md +9 -2
  156. package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +1 -1
  157. package/skills/weaver-e10-yimiaoban/references/msg-read.md +1 -1
  158. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +9 -9
  159. package/skills/weaver-e10-ziguanjia/SKILL.md +6 -1
  160. package/dist/shortcuts/invoice/operations/issuing.js +0 -198
  161. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-field-rules.md +0 -161
  162. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +0 -66
  163. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +0 -107
@@ -1,15 +1,16 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { InvoiceCliError, invoiceValidationError } from '../errors.js';
2
- import { SELLER_TAX_NO, makeInvoicePreview, normalizeMakeInvoicePayload, normalizeMode, } from './issuing.js';
3
- import { asNonEmptyString, assertBusinessSuccess, objectFingerprint, workflow } from './shared.js';
4
- const LEDGER_PATH = '/api/ebuilder/form/eblist/getData';
5
- const LEDGER_PAGE_ID = '1314988320430981804';
3
+ import { asNonEmptyString, assertBusinessSuccess } from './shared.js';
4
+ const LEDGER_PATH = '/api/ebuilder/form/list/getListDatas';
5
+ const LEDGER_LIST_ID = '1201416963938369627';
6
+ const LEDGER_PASS_ID = '1201416959744065575';
6
7
  const LEDGER_APP_ID = '846297243998732291';
7
- const LEDGER_MENU_ID = '5419015526244625718';
8
- const LEDGER_PAGE_TITLE = '5aWR57qm6ZSB5ZCI5ZCM5ZWG5ZOB5L h5oGv';
8
+ const LEDGER_MENU_ID = '5416292802968839298';
9
+ /** urlPageTitle:合同商品信息 */
10
+ const LEDGER_PAGE_TITLE = '5ZCI5ZCM5ZWG5ZOB5L+h5oGv';
9
11
  const LEDGER_PAGE_SIZE = 50;
10
12
  const LEDGER_MAX_PAGES = 40;
11
- const RESULT_PATH = '/api/inc/book/open/getMakeInvoiceResult';
12
- /** 契约锁合同商品信息表:数据 key → 可读字段。 */
13
+ /** 待开票合同数据:数据 key → 可读字段。含契约锁、泛微及其他合同。 */
13
14
  export const QYS_LEDGER_FIELDS = [
14
15
  { id: '1314988311673275798', key: 'contractNo', label: '合同号', kind: 'text' },
15
16
  { id: '1314988311673275801', key: 'contractFlow', label: '合同审批流程', kind: 'relation' },
@@ -92,15 +93,6 @@ function toCents(value, name) {
92
93
  function formatCents(cents) {
93
94
  return `${Math.floor(cents / 100)}.${String(cents % 100).padStart(2, '0')}`;
94
95
  }
95
- /** “6.0%” → 600(万分比),只接受开票接口支持的税率。 */
96
- export function parseTaxRateBasisPoints(name) {
97
- const match = typeof name === 'string' ? /^\s*(\d+(?:\.\d+)?)\s*%\s*$/u.exec(name) : null;
98
- const basisPoints = match ? Math.round(Number(match[1]) * 100) : Number.NaN;
99
- if (![0, 100, 300, 500, 600, 900, 1300].includes(basisPoints)) {
100
- throw invoiceValidationError('ledger_tax_rate_invalid', `商品税率无法识别或不在允许范围内:${name ?? '空'}`);
101
- }
102
- return basisPoints;
103
- }
104
96
  function isPositiveCents(value) {
105
97
  return value !== null && /^\d+(?:\.\d+)?$/u.test(value) && Number(value) > 0;
106
98
  }
@@ -117,49 +109,136 @@ export function qysRowBlockers(row) {
117
109
  blockers.push('已废弃');
118
110
  return blockers;
119
111
  }
120
- async function fetchLedgerPage(host, pageNo, operation) {
121
- const fieldFilter = {
122
- cusMenuId: LEDGER_MENU_ID,
123
- urlPageTitle: LEDGER_PAGE_TITLE,
124
- [`appId_${LEDGER_PAGE_ID}`]: LEDGER_APP_ID,
125
- cusview: '0',
112
+ function parseSpan(value) {
113
+ if (typeof value === 'string' && value.trim().startsWith('[')) {
114
+ try {
115
+ return optionValue(JSON.parse(value));
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ return optionValue(value);
122
+ }
123
+ function cellText(data, display, key) {
124
+ const raw = data[key] !== undefined && data[key] !== null && data[key] !== '' ? data[key] : display[key];
125
+ return textValue(raw);
126
+ }
127
+ function moneyText(data, display, key) {
128
+ const text = cellText(data, display, key);
129
+ return text === null ? null : text.replace(/,/g, '');
130
+ }
131
+ /** 获取待开票合同数据:getListDatas 的 data / displayData 行,转成与旧列表相同的可读字段。 */
132
+ export function convertPendingLedgerRow(data, display = {}) {
133
+ const formDataId = textValue(data.form_data_id) ?? textValue(data.id);
134
+ if (!formDataId)
135
+ throw invoiceValidationError('ledger_row_invalid', '台账行缺少 form_data_id');
136
+ const statusText = textValue(display.data_status);
137
+ return {
138
+ formDataId,
139
+ title: textValue(data.name) ?? textValue(display.name),
140
+ dataStatus: statusText && !/^\d+$/u.test(statusText) ? statusText : null,
141
+ contractNo: cellText(data, display, 'hth'),
142
+ contractFlow: parseSpan(display.htsplcSpan),
143
+ contractCategory: cellText(data, display, 'htzl'),
144
+ productType: parseSpan(display.cplxSpan),
145
+ productTypeDetail: cellText(data, display, 'cplxmx'),
146
+ accountingCompany: parseSpan(display.hsgs_idSpan),
147
+ invoiceGroupFlag: cellText(data, display, 'kpfzbs'),
148
+ sourceFlag: cellText(data, display, 'lybs'),
149
+ flowDetailId: cellText(data, display, 'lcmxid'),
150
+ invoiceType: parseSpan(display.fplxSpan),
151
+ purchaserName: cellText(data, display, 'gmfmc'),
152
+ purchaserTaxNo: cellText(data, display, 'gmfsh'),
153
+ purchaserAddress: cellText(data, display, 'gmfdz'),
154
+ purchaserTel: cellText(data, display, 'gmfdh'),
155
+ purchaserBankName: cellText(data, display, 'gmfkhx'),
156
+ purchaserBankAccount: cellText(data, display, 'gmfkhxzh'),
157
+ invoiceItemName: cellText(data, display, 'kpmc'),
158
+ goodsName: cellText(data, display, 'spmc'),
159
+ revenueCode: cellText(data, display, 'spshflbm'),
160
+ taxRate: parseSpan(display.spslSpan),
161
+ specification: cellText(data, display, 'ggxh'),
162
+ unit: cellText(data, display, 'dw'),
163
+ quantity: cellText(data, display, 'spslkwk'),
164
+ unitPrice: moneyText(data, display, 'spdjkwk'),
165
+ amount: moneyText(data, display, 'kpje'),
166
+ uninvoicedAmount: moneyText(data, display, 'wkpje'),
167
+ invoicedAmount: moneyText(data, display, 'ykpje'),
168
+ invoicingAmount: moneyText(data, display, 'kpzje'),
169
+ amountDiffers: parseSpan(display.kpjeczcySpan),
170
+ thirdPartyPurchase: parseSpan(display.sfsfcgSpan),
171
+ invoiceNo: cellText(data, display, 'fphm'),
172
+ invoiceFlow: parseSpan(display.kplcidSpan),
173
+ invoiceDate: cellText(data, display, 'kpsj'),
174
+ discarded: parseSpan(display.sfzfSpan),
175
+ discardedDate: cellText(data, display, 'zfsj'),
176
+ contactName: cellText(data, display, 'khlxr'),
177
+ contactPhone: cellText(data, display, 'khlxdh'),
178
+ contactEmail: cellText(data, display, 'khlxyx'),
126
179
  };
127
- const result = await host.invoiceMultipartRequest({
180
+ }
181
+ async function fetchLedgerPage(host, pageNo, tableuid, operation) {
182
+ const result = await host.invoiceRequest({
128
183
  path: LEDGER_PATH,
129
- headers: { ebbusinessid: LEDGER_PAGE_ID },
130
- fields: {
131
- ...fieldFilter,
132
- filter: '[]',
133
- fieldFilter,
134
- pageFilter: { filter: {}, filterIsNull: true, esbFilter: [] },
135
- pageAppId: LEDGER_APP_ID,
136
- compId: 'e00e8244de584b369417a4fea31d7bed',
137
- pageId: LEDGER_PAGE_ID,
138
- pageNo: String(pageNo),
139
- pageSize: String(LEDGER_PAGE_SIZE),
140
- tableuid: 'a74d995bddd54a4f831551b468071632',
141
- refPluginPackageId: '933846626099339271',
142
- },
184
+ query: { passid: LEDGER_PASS_ID, cid: LEDGER_PASS_ID },
185
+ headers: { ebbusinessid: LEDGER_LIST_ID },
143
186
  operation,
187
+ body: {
188
+ current: pageNo,
189
+ pageSize: LEDGER_PAGE_SIZE,
190
+ requiredMatch: 1,
191
+ tableuid,
192
+ mapResult: '1',
193
+ recheckQuery: '1',
194
+ cusMenuId: LEDGER_MENU_ID,
195
+ urlPageTitle: LEDGER_PAGE_TITLE,
196
+ [`appId_${LEDGER_LIST_ID}`]: LEDGER_APP_ID,
197
+ searchParamData: {
198
+ searchType: '3',
199
+ quickSearchFields: {},
200
+ commonSearchFields: {},
201
+ type: 'and',
202
+ groupFields: {},
203
+ },
204
+ listId: LEDGER_LIST_ID,
205
+ isFollow: 0,
206
+ isShareToMe: 0,
207
+ filterCondition: {},
208
+ browser: 'Chrome',
209
+ },
144
210
  });
145
- assertBusinessSuccess(result, 'qys_ledger_query_failed', '契约锁开票台账查询失败');
146
- const list = result?.data?.list;
147
- if (!Array.isArray(list)) {
148
- throw new InvoiceCliError('api', 'qys_ledger_query_failed', '契约锁开票台账返回缺少 data.list', { exitCode: 1 });
211
+ assertBusinessSuccess(result, 'qys_ledger_query_failed', '获取待开票合同数据失败');
212
+ const payload = result?.data;
213
+ const dataRows = payload?.data;
214
+ const displayRows = payload?.displayData;
215
+ if (!Array.isArray(dataRows) || !Array.isArray(displayRows)) {
216
+ throw new InvoiceCliError('api', 'qys_ledger_query_failed', '获取待开票合同数据的返回缺少 data.data 或 data.displayData', { exitCode: 1 });
149
217
  }
150
- return list;
218
+ return dataRows.map((data, index) => ({
219
+ data,
220
+ display: displayRows[index] && typeof displayRows[index] === 'object' ? displayRows[index] : {},
221
+ }));
151
222
  }
152
- /** 接口返回的 count 恒为 0,只能按“本页未取满”判断结束。 */
223
+ /** total / hasMore 不可靠,按本页未取满或出现重复行结束。 */
153
224
  async function scanLedger(host, operation, visit) {
225
+ const tableuid = randomUUID().replace(/-/g, '');
226
+ const seen = new Set();
154
227
  let scanned = 0;
155
228
  for (let pageNo = 1; pageNo <= LEDGER_MAX_PAGES; pageNo += 1) {
156
- const page = await fetchLedgerPage(host, pageNo, operation);
157
- for (const raw of page) {
229
+ const page = await fetchLedgerPage(host, pageNo, tableuid, operation);
230
+ let fresh = 0;
231
+ for (const pair of page) {
232
+ const row = convertPendingLedgerRow(pair.data, pair.display);
233
+ if (seen.has(row.formDataId))
234
+ continue;
235
+ seen.add(row.formDataId);
236
+ fresh += 1;
158
237
  scanned += 1;
159
- if (visit(convertQysLedgerRow(raw)))
238
+ if (visit(row))
160
239
  return { scanned, complete: true };
161
240
  }
162
- if (page.length < LEDGER_PAGE_SIZE)
241
+ if (page.length < LEDGER_PAGE_SIZE || fresh === 0)
163
242
  return { scanned, complete: true };
164
243
  }
165
244
  return { scanned, complete: false };
@@ -168,6 +247,142 @@ function matchesKeyword(row, keyword) {
168
247
  const haystack = [row.contractNo, row.purchaserName, row.contractFlow?.name, row.goodsName, row.invoiceItemName, row.formDataId];
169
248
  return haystack.some((value) => typeof value === 'string' && value.includes(keyword));
170
249
  }
250
+ /**
251
+ * 台账常把开户行名称和账号一起写进「购买方开户行账号」(如 `招商银行福田支行 8135 8668 8310 001`)。
252
+ * 拆成名称与账号两段:账号取末尾(或开头)连续的数字段,原样保留其中的空格和连字符。
253
+ */
254
+ export function splitBankNameAndAccount(bankName, bankAccount) {
255
+ const name = (bankName ?? '').trim();
256
+ const account = (bankAccount ?? '').trim();
257
+ const trailing = /^(.*?[^\d\s-])\s*(\d[\d\s-]*\d)$/u.exec(account);
258
+ const leading = trailing ? null : /^(\d[\d\s-]*\d)\s*([^\d\s-].*)$/u.exec(account);
259
+ const rawText = trailing ? trailing[1] : leading ? leading[2] : '';
260
+ const textPart = rawText
261
+ .replace(/[\s,,;;::]*(?:银行账号|银行帐号|账号|帐号|账户|帐户)?[\s,,;;::]*$/u, '')
262
+ .replace(/^[\s,,;;::]*(?:开户行|开户银行)?[\s,,;;::]*/u, '')
263
+ .trim();
264
+ const digitPart = trailing ? trailing[2] : leading ? leading[1] : '';
265
+ if (!textPart || !digitPart)
266
+ return { bankName: name, bankAccount: account, split: false };
267
+ return { bankName: name || textPart, bankAccount: digitPart, split: true };
268
+ }
269
+ export function isSpecialInvoiceTypeName(name) {
270
+ return name.includes('专票') || name.includes('专用发票');
271
+ }
272
+ /** 一张开票流程对应一个“合同号 + 购方”分组;金额只累计可开票行。 */
273
+ export class QysLedgerGrouper {
274
+ groups = new Map();
275
+ add(row, blocked) {
276
+ const contractNo = row.contractNo ?? '';
277
+ const purchaserName = row.purchaserName ?? '';
278
+ const key = `${contractNo}|${purchaserName}`;
279
+ let group = this.groups.get(key);
280
+ if (!group) {
281
+ group = {
282
+ contractNo,
283
+ contractFlowId: row.contractFlow?.id ?? '',
284
+ contractFlowName: row.contractFlow?.name ?? '',
285
+ purchaserName,
286
+ purchaserTaxNos: new Set(),
287
+ invoiceTypes: new Set(),
288
+ goodsNames: new Set(),
289
+ taxRates: new Set(),
290
+ specifications: new Set(),
291
+ sellerNames: new Set(),
292
+ bankName: null,
293
+ bankAccount: null,
294
+ formDataIds: [],
295
+ amountCents: 0,
296
+ invalidAmountRows: [],
297
+ blockedRows: 0,
298
+ };
299
+ this.groups.set(key, group);
300
+ }
301
+ if (row.accountingCompany?.name)
302
+ group.sellerNames.add(row.accountingCompany.name);
303
+ if (blocked) {
304
+ group.blockedRows += 1;
305
+ return;
306
+ }
307
+ group.formDataIds.push(row.formDataId);
308
+ if (!group.contractFlowId && row.contractFlow) {
309
+ group.contractFlowId = row.contractFlow.id;
310
+ group.contractFlowName = row.contractFlow.name;
311
+ }
312
+ if (row.purchaserTaxNo)
313
+ group.purchaserTaxNos.add(row.purchaserTaxNo);
314
+ if (row.invoiceType?.name)
315
+ group.invoiceTypes.add(row.invoiceType.name);
316
+ const goodsName = row.invoiceItemName ?? row.goodsName;
317
+ if (goodsName)
318
+ group.goodsNames.add(goodsName);
319
+ if (row.taxRate?.name)
320
+ group.taxRates.add(row.taxRate.name);
321
+ if (row.specification)
322
+ group.specifications.add(row.specification);
323
+ if (group.bankName === null && group.bankAccount === null && (row.purchaserBankName || row.purchaserBankAccount)) {
324
+ group.bankName = row.purchaserBankName ?? null;
325
+ group.bankAccount = row.purchaserBankAccount ?? null;
326
+ }
327
+ if (row.amount !== null && /^\d+(?:\.\d{1,2})?$/u.test(row.amount)) {
328
+ group.amountCents += toCents(row.amount, '开票金额');
329
+ }
330
+ else {
331
+ group.invalidAmountRows.push(row.formDataId);
332
+ }
333
+ }
334
+ result(includeEmpty) {
335
+ const output = [];
336
+ for (const group of this.groups.values()) {
337
+ if (!includeEmpty && group.formDataIds.length === 0)
338
+ continue;
339
+ const warnings = [];
340
+ const specialTypes = [...group.invoiceTypes].filter(isSpecialInvoiceTypeName);
341
+ const specialInvoiceWithoutTaxNo = group.purchaserTaxNos.size === 0 && specialTypes.length > 0;
342
+ if (specialInvoiceWithoutTaxNo) {
343
+ warnings.push(`购方无税号,不能开专票(台账发票类型:${specialTypes.join('、')}),请先在台账改为普票再发起`);
344
+ }
345
+ const bank = splitBankNameAndAccount(group.bankName, group.bankAccount);
346
+ if (bank.split) {
347
+ warnings.push(`台账「购买方开户行账号」混有开户行名称,已拆分为开户行名称「${bank.bankName}」、账号「${bank.bankAccount}」`);
348
+ }
349
+ if (!group.contractNo)
350
+ warnings.push('台账行缺少合同号,无法按合同号发起开票流程');
351
+ if (group.sellerNames.size > 1)
352
+ warnings.push(`同组销售方(核算公司)不一致:${[...group.sellerNames].join('、')}`);
353
+ if (group.invoiceTypes.size > 1)
354
+ warnings.push(`同组发票类型不一致:${[...group.invoiceTypes].join('、')}`);
355
+ if (group.purchaserTaxNos.size > 1)
356
+ warnings.push(`同组购方税号不一致:${[...group.purchaserTaxNos].join('、')}`);
357
+ if (group.invalidAmountRows.length)
358
+ warnings.push(`开票金额无法识别,未计入合计:${group.invalidAmountRows.join('、')}`);
359
+ if (group.blockedRows > 0) {
360
+ warnings.push(`同合同另有 ${group.blockedRows} 行已开票、开票中或已废弃,流程联动可能一并带出,请在 E10 核对明细`);
361
+ }
362
+ output.push({
363
+ contractNo: group.contractNo,
364
+ contractFlowId: group.contractFlowId,
365
+ contractFlowName: group.contractFlowName,
366
+ purchaserName: group.purchaserName,
367
+ purchaserTaxNo: [...group.purchaserTaxNos].join('、'),
368
+ purchaserBankName: bank.bankName,
369
+ purchaserBankAccount: bank.bankAccount,
370
+ invoiceTypes: [...group.invoiceTypes],
371
+ specialInvoiceWithoutTaxNo,
372
+ goodsNames: [...group.goodsNames],
373
+ taxRates: [...group.taxRates],
374
+ specifications: [...group.specifications],
375
+ sellerNames: [...group.sellerNames],
376
+ rowCount: group.formDataIds.length,
377
+ formDataIds: group.formDataIds,
378
+ totalAmount: formatCents(group.amountCents),
379
+ blockedRows: group.blockedRows,
380
+ warnings,
381
+ });
382
+ }
383
+ return output;
384
+ }
385
+ }
171
386
  export class InvoiceIssuingQysListOperation {
172
387
  name = 'invoice.issuing.qys.list';
173
388
  async execute(input, { host }) {
@@ -178,6 +393,7 @@ export class InvoiceIssuingQysListOperation {
178
393
  if (!Number.isInteger(limit) || limit < 1 || limit > 100)
179
394
  throw invoiceValidationError('input_invalid', 'limit 必须是 1-100 的整数');
180
395
  const items = [];
396
+ const grouper = new QysLedgerGrouper();
181
397
  let matched = 0;
182
398
  const scan = await scanLedger(host, this.name, (row) => {
183
399
  if (contractNo && row.contractNo !== contractNo)
@@ -185,6 +401,7 @@ export class InvoiceIssuingQysListOperation {
185
401
  if (keyword && !matchesKeyword(row, keyword))
186
402
  return false;
187
403
  const blockers = qysRowBlockers(row);
404
+ grouper.add(row, blockers.length > 0);
188
405
  if (!includeIssued && blockers.length)
189
406
  return false;
190
407
  matched += 1;
@@ -196,6 +413,7 @@ export class InvoiceIssuingQysListOperation {
196
413
  status: 'COMPLETE',
197
414
  data: {
198
415
  items,
416
+ groups: grouper.result(includeIssued),
199
417
  matched,
200
418
  truncated: matched > items.length,
201
419
  scanned: scan.scanned,
@@ -204,158 +422,3 @@ export class InvoiceIssuingQysListOperation {
204
422
  };
205
423
  }
206
424
  }
207
- export function buildQysMakeInvoicePayload(row) {
208
- const warnings = [];
209
- const invoiceType = row.invoiceType?.id;
210
- if (!invoiceType)
211
- throw invoiceValidationError('ledger_field_missing', '台账行缺少发票类型');
212
- const goodsName = row.invoiceItemName ?? row.goodsName;
213
- if (!goodsName)
214
- throw invoiceValidationError('ledger_field_missing', '台账行缺少开票名称和商品名称');
215
- if (!row.purchaserName)
216
- throw invoiceValidationError('ledger_field_missing', '台账行缺少购买方名称');
217
- const basisPoints = parseTaxRateBasisPoints(row.taxRate?.name);
218
- const amountCents = toCents(row.amount, '开票金额');
219
- if (amountCents === 0)
220
- throw invoiceValidationError('ledger_amount_invalid', '开票金额必须大于 0');
221
- const taxCents = Math.floor((amountCents * basisPoints + 5000) / 10000);
222
- const amount = formatCents(amountCents);
223
- const taxAmount = formatCents(taxCents);
224
- const amountWithTax = formatCents(amountCents + taxCents);
225
- if (row.quantity !== null && row.unitPrice !== null) {
226
- const expected = Number(row.quantity) * Number(row.unitPrice);
227
- if (!Number.isFinite(expected) || Math.abs(expected * 100 - amountCents) > 1) {
228
- warnings.push(`商品数量 ${row.quantity} × 商品单价 ${row.unitPrice} 与开票金额 ${row.amount} 不一致`);
229
- }
230
- }
231
- if (row.uninvoicedAmount !== null && row.uninvoicedAmount !== row.amount && Number(row.uninvoicedAmount) !== Number(row.amount)) {
232
- warnings.push(`未开票金额 ${row.uninvoicedAmount} 与开票金额 ${row.amount} 不一致,本次按开票金额全额开具`);
233
- }
234
- if (row.dataStatus && row.dataStatus !== '提交')
235
- warnings.push(`台账数据状态为“${row.dataStatus}”`);
236
- const detail = {
237
- goodsName,
238
- taxSign: '0',
239
- taxRate: basisPoints / 10000,
240
- amount,
241
- taxAmount,
242
- amountWithTax,
243
- };
244
- for (const key of ['revenueCode', 'specification', 'unit', 'quantity', 'unitPrice']) {
245
- if (row[key] !== null)
246
- detail[key] = row[key];
247
- }
248
- const payload = {
249
- invoiceType,
250
- externalDocumentNo: `QYS-${row.formDataId}`,
251
- purchaserName: row.purchaserName,
252
- taxSign: '0',
253
- totalAmount: amount,
254
- taxAmount,
255
- totalAmountWithTax: amountWithTax,
256
- details: [detail],
257
- };
258
- for (const key of ['purchaserTaxNo', 'purchaserAddress', 'purchaserTel', 'purchaserBankName', 'purchaserBankAccount']) {
259
- if (row[key] !== null)
260
- payload[key] = row[key];
261
- }
262
- return { payload, warnings };
263
- }
264
- export class InvoiceIssuingQysPrepareOperation {
265
- name = 'invoice.issuing.qys.prepare';
266
- async execute(input, { host, continuation }) {
267
- const mode = normalizeMode(input.mode);
268
- const formDataId = asNonEmptyString(input.formDataId, 'formDataId');
269
- if (!/^\d+$/u.test(formDataId))
270
- throw invoiceValidationError('input_invalid', 'formDataId 必须是台账行的数字 ID');
271
- let row;
272
- const scan = await scanLedger(host, this.name, (candidate) => {
273
- if (candidate.formDataId !== formDataId)
274
- return false;
275
- row = candidate;
276
- return true;
277
- });
278
- if (!row) {
279
- throw invoiceValidationError('ledger_row_not_found', `契约锁开票台账中未找到 formDataId=${formDataId} 的行`, {
280
- scanned: scan.scanned,
281
- scanComplete: scan.complete,
282
- });
283
- }
284
- const blockers = qysRowBlockers(row);
285
- if (blockers.length) {
286
- throw invoiceValidationError('ledger_row_not_issuable', `该台账行不能开票:${blockers.join(';')}`, { formDataId, blockers });
287
- }
288
- const { payload, warnings } = buildQysMakeInvoicePayload(row);
289
- const normalizedPayload = normalizeMakeInvoicePayload(payload, mode);
290
- const preview = {
291
- ...makeInvoicePreview(normalizedPayload, mode),
292
- formDataId,
293
- contractNo: row.contractNo ?? '',
294
- contractFlowName: row.contractFlow?.name ?? '',
295
- invoiceTypeName: row.invoiceType?.name ?? '',
296
- goodsName: normalizedPayload.details[0].goodsName,
297
- taxRate: row.taxRate?.name ?? '',
298
- };
299
- const session = await host.getSession();
300
- const continuationToken = await continuation.issue({
301
- operation: 'invoice.issuing.make.apply',
302
- workflow: workflow('invoice.issuing.make', 'AWAITING_CONFIRMATION'),
303
- baseUrl: session.baseUrl,
304
- profile: session.profile,
305
- userId: session.userId,
306
- mode,
307
- fingerprint: objectFingerprint(normalizedPayload),
308
- preview,
309
- });
310
- return {
311
- status: 'AWAITING_CONFIRMATION',
312
- data: {
313
- preview,
314
- warnings,
315
- ledgerRow: row,
316
- normalizedPayload,
317
- continuation: continuationToken,
318
- expiresInSeconds: 600,
319
- workflow: workflow('invoice.issuing.make', 'AWAITING_CONFIRMATION'),
320
- },
321
- };
322
- }
323
- }
324
- function resultState(returnCode) {
325
- if (returnCode === '00')
326
- return 'SUCCESS';
327
- if (returnCode === '01')
328
- return 'PENDING';
329
- return 'FAILED';
330
- }
331
- export class InvoiceIssuingResultOperation {
332
- name = 'invoice.issuing.result';
333
- async execute(input, { host }) {
334
- const externalDocumentNo = asNonEmptyString(input.externalDocumentNo, 'externalDocumentNo');
335
- const invoiceReq = { businessNo: externalDocumentNo, taxNo: SELLER_TAX_NO };
336
- if (input.serialNo !== undefined && input.serialNo !== '')
337
- invoiceReq.serialNo = asNonEmptyString(input.serialNo, 'serialNo');
338
- const result = await host.invoiceRequest({
339
- path: RESULT_PATH,
340
- method: 'POST',
341
- body: { invoiceReq },
342
- operation: this.name,
343
- timeoutMs: 60_000,
344
- });
345
- assertBusinessSuccess(result, 'issuing_result_failed', '查询开票结果失败');
346
- const data = result?.data || {};
347
- return {
348
- status: 'COMPLETE',
349
- data: {
350
- externalDocumentNo,
351
- state: resultState(data.returnCode),
352
- returnCode: data.returnCode ?? '',
353
- returnMessage: data.returnMessage ?? '',
354
- invoiceNo: data.digitInvoiceNo || data.invoiceNo || '',
355
- invoiceTime: data.invoiceTime ?? '',
356
- serialNo: data.serialNo ?? '',
357
- result,
358
- },
359
- };
360
- }
361
- }
@@ -6,8 +6,7 @@ import { InvoiceDeleteApplyOperation, InvoiceDeletePrepareOperation } from './de
6
6
  import { InvoiceDownloadOperation } from './download.js';
7
7
  import { InvoiceGetOperation } from './get.js';
8
8
  import { InvoiceImportApplyOperation, InvoiceImportPrepareOperation } from './import.js';
9
- import { InvoiceIssuingMakeApplyOperation, InvoiceIssuingMakePrepareOperation } from './issuing.js';
10
- import { InvoiceIssuingQysListOperation, InvoiceIssuingQysPrepareOperation, InvoiceIssuingResultOperation, } from './issuing-qys.js';
9
+ import { InvoiceIssuingQysListOperation } from './issuing-qys.js';
11
10
  import { InvoiceListOperation } from './list.js';
12
11
  import { InvoiceOcrPreviewOperation } from './ocr-preview.js';
13
12
  import { InvoiceReimEmployeeSuperiorsOperation, InvoiceReimFieldSearchOperation, InvoiceReimFileOcrPreviewOperation, InvoiceReimFlowCreateApplyOperation, InvoiceReimFlowCreatePrepareOperation, InvoiceReimFlowUpdateApplyOperation, InvoiceReimFlowUpdatePrepareOperation, InvoiceReimFormStructureOperation, InvoiceReimRequestsListOperation, InvoiceReimRowInfoBatchOperation, InvoiceReimRowInfoOperation, InvoiceReimWorkflowResolveOperation, } from './reim.js';
@@ -51,11 +50,7 @@ function buildRegistry() {
51
50
  new InvoiceReimFlowCreateApplyOperation(),
52
51
  new InvoiceReimFlowUpdatePrepareOperation(),
53
52
  new InvoiceReimFlowUpdateApplyOperation(),
54
- new InvoiceIssuingMakePrepareOperation(),
55
- new InvoiceIssuingMakeApplyOperation(),
56
53
  new InvoiceIssuingQysListOperation(),
57
- new InvoiceIssuingQysPrepareOperation(),
58
- new InvoiceIssuingResultOperation(),
59
54
  ];
60
55
  for (const handler of handlers) {
61
56
  if (registry.has(handler.name))
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -127,7 +128,7 @@ export function createJiuchuanhuiHost(ctx, dependencies = {}) {
127
128
  mapRuntimeNetworkError(error);
128
129
  }
129
130
  if (response.status === 401 || response.status === 302) {
130
- throw jiuchuanhuiAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> --agent_type <当前智能体名称> 完成登录', { httpStatus: response.status });
131
+ throw jiuchuanhuiAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
131
132
  }
132
133
  if (response.parseError) {
133
134
  if (response.status < 200 || response.status >= 300) {
@@ -1,4 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
2
3
  import { requireE10Session } from '../../internal/e10/context.js';
3
4
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
4
5
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -149,7 +150,7 @@ export function createMailHost(ctx, dependencies = {}) {
149
150
  throw error;
150
151
  }
151
152
  if (response.status === 401 || response.status === 302) {
152
- throw mailAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
153
+ throw mailAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
153
154
  }
154
155
  if (response.parseError) {
155
156
  if (response.status < 200 || response.status >= 300) {
@@ -1,3 +1,4 @@
1
+ import { E10_SESSION_EXPIRED_MESSAGE } from '../../internal/e10/auth/messages.js';
1
2
  import { requireE10Session } from '../../internal/e10/context.js';
2
3
  import { getActiveProfile } from '../../internal/e10/auth/session.js';
3
4
  import { E10ApiError, createE10RequestRuntime } from '../../internal/e10/request-runtime.js';
@@ -142,7 +143,7 @@ export function createMeetingHost(ctx, dependencies = {}) {
142
143
  mapRuntimeNetworkError(error);
143
144
  }
144
145
  if (response.status === 401 || response.status === 302) {
145
- throw meetingAuthError('session_expired', 'E10 登录态已失效,请先向用户确认 E10 登录域名,再运行 weaver-work-cli auth login --base-url <域名> 完成登录', { httpStatus: response.status });
146
+ throw meetingAuthError('session_expired', E10_SESSION_EXPIRED_MESSAGE, { httpStatus: response.status });
146
147
  }
147
148
  if (response.parseError) {
148
149
  if (response.status < 200 || response.status >= 300) {