@xtalpi/agentic-lab-skills 0.0.9 → 0.0.11

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 (68) hide show
  1. package/README.md +1 -1
  2. package/package.json +14 -14
  3. package/skills/lab-flow-designer/SKILL.md +612 -593
  4. package/skills/lab-flow-designer/embedded-template/SKILL.md +103 -88
  5. package/skills/lab-flow-designer/embedded-template/pools//345/205/245/345/217/243/346/261/240.md +21 -12
  6. package/skills/lab-flow-designer/embedded-template/pools//345/207/272/345/217/243/346/261/240.md +21 -12
  7. package/skills/lab-flow-designer/embedded-template/scripts//347/244/272/344/276/213/346/225/260/346/215/256/344/270/216/346/240/241/351/252/214/351/227/250/346/216/247.js +142 -142
  8. package/skills/lab-flow-designer/embedded-template/valves//347/244/272/344/276/213/346/225/260/346/215/256/344/270/216/346/240/241/351/252/214/351/227/250/346/216/247.md +114 -99
  9. package/skills/lab-flow-designer/references/agentic-lab-processer.md +122 -78
  10. package/skills/lab-flow-designer/references/agentic-lab-sdk.md +534 -361
  11. package/skills/lab-flow-designer/references/rhea-api/README.md +7 -7
  12. package/skills/lab-flow-designer/references/rhea-api/execute_process_batch.md +58 -58
  13. package/skills/lab-flow-designer/references/skill-package-layout.md +268 -204
  14. package/skills/lab-flow-designer/references//344/270/232/345/212/241/346/265/201/347/250/213/346/226/207/346/241/243/346/240/207/345/207/206.md +216 -208
  15. package/skills/lab-flow-designer/templates//344/270/232/345/212/241/346/265/201/347/250/213/346/226/207/346/241/243/346/250/241/346/235/277.md +192 -169
  16. package/skills/lab-flow-designer/templates//344/270/232/345/212/241/346/265/201/347/250/213/346/226/207/346/241/243/347/244/272/344/276/213.md +207 -197
  17. package/skills/lab-flow-designer/testing/test-processer.mjs +1240 -1075
  18. package/skills/lab-nocobase-flow-generator/SKILL.md +164 -164
  19. package/skills/lab-nocobase-flow-generator/examples/setting/350/241/250/350/216/267/345/217/226/345/244/226/351/203/250/346/234/215/345/212/241.js +70 -70
  20. package/skills/lab-nocobase-flow-generator/examples//346/237/245/350/257/242/345/214/226/345/255/246/345/223/201/344/277/241/346/201/257.js +30 -30
  21. package/skills/lab-nocobase-flow-generator/references/doc-standard.md +84 -84
  22. package/skills/lab-nocobase-flow-generator/references/runtime-api.md +224 -224
  23. package/skills/lab-nocobase-flow-generator/templates//350/204/232/346/234/254/351/200/273/350/276/221/346/226/207/346/241/243/346/250/241/346/235/277.md +121 -121
  24. package/skills/lab-nocobase-flow-generator/templates//350/204/232/346/234/254/351/200/273/350/276/221/346/226/207/346/241/243/347/244/272/344/276/213.md +67 -67
  25. package/skills/lab-orbit-component-builder/SKILL.md +353 -353
  26. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.env.local.example +27 -27
  27. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.eslintignore +7 -7
  28. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.eslintrc.cjs +88 -88
  29. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.nvmrc +1 -1
  30. package/skills/lab-orbit-component-builder/examples/xnb-component-template/AgenticAppAPI.md +268 -268
  31. package/skills/lab-orbit-component-builder/examples/xnb-component-template/Jenkinsfile +106 -106
  32. package/skills/lab-orbit-component-builder/examples/xnb-component-template/OrbitAPI.md +453 -453
  33. package/skills/lab-orbit-component-builder/examples/xnb-component-template/README.md +176 -176
  34. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/public/index.html +12 -12
  35. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/App.vue +151 -151
  36. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/components/DevOpenerLauncher.vue +143 -143
  37. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/global.d.ts +77 -77
  38. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/main.ts +308 -308
  39. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/mockXNBBitable.ts +119 -119
  40. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/shims-vue.d.ts +6 -6
  41. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/utils/devOpenerHost.ts +75 -75
  42. package/skills/lab-orbit-component-builder/examples/xnb-component-template/index.html +13 -13
  43. package/skills/lab-orbit-component-builder/examples/xnb-component-template/package.json +60 -60
  44. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/api/agenticlabTickets.ts +110 -110
  45. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/entries/bitable.ts +4 -4
  46. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/entries/custom-page.ts +4 -4
  47. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/index.ts +1 -1
  48. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/styles/orbit-quasar-host.scss +19 -19
  49. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/types/context.ts +15 -15
  50. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/types/xnb-context.ts +70 -70
  51. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useBitablePage.ts +189 -189
  52. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useSuperCellDemo.ts +257 -257
  53. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useSuperTableBitableLifecycle.ts +555 -555
  54. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/openerInitParams.ts +158 -158
  55. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/openerTicketIds.ts +32 -32
  56. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/orbitHttpClient.ts +110 -110
  57. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/request.ts +92 -92
  58. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/views/bitable.vue +67 -67
  59. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/views/custom-page.vue +140 -140
  60. package/skills/lab-orbit-component-builder/examples/xnb-component-template/tsconfig.json +45 -45
  61. package/skills/lab-orbit-component-builder/examples/xnb-component-template/vite.config.ts +170 -170
  62. package/skills/lab-orbit-component-builder/examples/xnb-component-template/vite.dev.config.ts +58 -58
  63. package/skills/lab-orbit-component-builder/references/flow-document-human-ui.md +65 -65
  64. package/skills/lab-orbit-component-builder/references/orbit-vue-conventions.md +133 -133
  65. package/skills/lab-orbit-component-builder/references/pool-schema-to-columns.md +67 -67
  66. package/skills/lab-orbit-component-builder/references/vue-template-checklist.md +179 -179
  67. package/skills/lab-orbit-component-builder/references/xnb-context-vue-props.md +49 -49
  68. package/skills/lab-orbit-component-builder/references/xnbitable-vue-parity.md +32 -32
@@ -1,555 +1,555 @@
1
- /* eslint-disable @typescript-eslint/typedef, prefer-destructuring, no-continue, quote-props, operator-linebreak */
2
- import { computed, ref, watch } from 'vue'
3
- import type { Ref } from 'vue'
4
- import { fetchAgenticlabTicketList, postAgenticlabExecutionComplete } from '@/api/agenticlabTickets'
5
- import type { XNBBitableChoreo, XnbSuperCellContext } from '@/types/xnb-context'
6
- import { extractTicketIdsFromPayload, extractValveIdFromPayload } from '@/utils/openerTicketIds'
7
-
8
- /** 与 Book 子表、updateSubBitable 最后一参 `name` 对齐(示例名) */
9
- export const DEMO_SUB_TABLE_NAME = '演示'
10
-
11
- /** index 须与子表编辑器一致;单主表且编辑器为空时可试 `''`(多子表勿用空串) */
12
- export const DEMO_LIST_SUB = { index: 'sheet-demo', name: DEMO_SUB_TABLE_NAME, row: 200 } as const
13
-
14
- const HEADER_GRAY = '#9e9e9e'
15
-
16
- /** 列定义:field 为表格中文列头,key 为接口/内存行字段 */
17
- export const DEMO_LIST_COLS = [
18
- { field: '编码', key: 'code' },
19
- { field: '名称', key: 'name' },
20
- { field: '数量', key: 'qty' }
21
- ] as const
22
-
23
- export interface DemoListRow {
24
- id: string
25
- code: string
26
- name: string
27
- qty: number
28
- }
29
-
30
- const bitableConfig = {
31
- columnlen: { '0': 120, '1': 160, '2': 100 },
32
- rowlen: { 0: 34 }
33
- }
34
-
35
- function buildDataColumns (): Array<Record<string, unknown>> {
36
- const cols: Array<Record<string, unknown>> = [
37
- {
38
- w: 120,
39
- dv: { type: 'text_length' },
40
- field: '编码',
41
- readonly: true,
42
- visible: true,
43
- dbRelated: false,
44
- filters: null,
45
- configurable: false,
46
- headerColor: HEADER_GRAY
47
- },
48
- {
49
- w: 160,
50
- dv: { type: 'text_length' },
51
- field: '名称',
52
- readonly: true,
53
- visible: true,
54
- dbRelated: false,
55
- filters: null,
56
- configurable: false,
57
- headerColor: HEADER_GRAY
58
- },
59
- {
60
- w: 100,
61
- dv: { type: 'number' },
62
- ct: { fa: '0', t: 'n' },
63
- field: '数量',
64
- readonly: true,
65
- visible: true,
66
- dbRelated: false,
67
- filters: null,
68
- configurable: false,
69
- headerColor: HEADER_GRAY
70
- }
71
- ]
72
- cols.forEach((dc, i) => {
73
- dc.c = i
74
- })
75
- return cols
76
- }
77
-
78
- const dataColumns = buildDataColumns()
79
-
80
- /** opener `payload` 中带 `demoRows` 时用于首屏/刷新数据源(类型宽松便于对接) */
81
- export function parseDemoRowsFromOpenerPayload (payload: unknown): DemoListRow[] | null {
82
- if (payload == null || typeof payload !== 'object') return null
83
- const rows = (payload as { demoRows?: unknown }).demoRows
84
- if (!Array.isArray(rows) || rows.length === 0) return null
85
- const out: DemoListRow[] = []
86
- for (const r of rows) {
87
- if (r == null || typeof r !== 'object') continue
88
- const o = r as Record<string, unknown>
89
- const id = o.id != null ? String(o.id) : ''
90
- if (!id) continue
91
- const code = o.code != null ? String(o.code) : ''
92
- const name = o.name != null ? String(o.name) : ''
93
- const qty = typeof o.qty === 'number' ? o.qty : Number(o.qty)
94
- if (!Number.isFinite(qty)) continue
95
- out.push({ id, code, name, qty })
96
- }
97
- return out.length ? out : null
98
- }
99
-
100
- /** opener 已预拉工单列表时(如父级合并的 `ticketList`),直接映射为表格行 */
101
- function parseTicketListFromOpenerPayload (payload: unknown): DemoListRow[] | null {
102
- if (payload == null || typeof payload !== 'object') return null
103
- const list = (payload as { ticketList?: unknown }).ticketList
104
- if (!Array.isArray(list) || list.length === 0) return null
105
- const rows = list.map((t) => mapAgenticlabTicketToDemoRow(t)).filter((r): r is DemoListRow => r != null)
106
- return rows.length ? rows : null
107
- }
108
-
109
- function numOr0 (v: unknown): number {
110
- const n = typeof v === 'number' ? v : Number(v)
111
- return Number.isFinite(n) ? n : 0
112
- }
113
-
114
- /** 将 `/api/ticket/list` 单项映射为示例表列(编码/名称/数量) */
115
- function mapAgenticlabTicketToDemoRow (raw: unknown): DemoListRow | null {
116
- if (raw == null || typeof raw !== 'object') return null
117
- const t = raw as Record<string, unknown>
118
- const id = t.id != null ? String(t.id) : ''
119
- if (!id) return null
120
- const detail = t.detail != null && typeof t.detail === 'object' ? (t.detail as Record<string, unknown>) : {}
121
- const ticketId = detail.ticket_id != null ? String(detail.ticket_id) : id
122
- const name =
123
- detail.compound_id != null
124
- ? String(detail.compound_id)
125
- : detail.smiles != null
126
- ? String(detail.smiles)
127
- : String(t.uuid ?? ticketId)
128
- const pp = detail.process_params != null && typeof detail.process_params === 'object'
129
- ? (detail.process_params as Record<string, unknown>)
130
- : {}
131
- const qty =
132
- numOr0(pp['@requested_amount_mg']) ||
133
- numOr0(detail.target_amount_1_mg) ||
134
- numOr0(detail.target_amount_2_mg) ||
135
- 0
136
- return { id, code: ticketId, name, qty }
137
- }
138
-
139
- export interface ListSubShape {
140
- index: string
141
- name: string
142
- row: number
143
- }
144
-
145
- function handleBitable (bitable: Record<string, unknown>, sub: ListSubShape | undefined): void {
146
- bitable.name = sub?.name
147
- if (sub && Object.prototype.hasOwnProperty.call(sub, 'index')) bitable.index = sub.index
148
- bitable.row = sub?.row ?? 200
149
- bitable.xnb_show_sheetbar = true
150
- bitable.xnb_hide_statisticbar = true
151
- bitable.xnb_hide_toolbar = false
152
- bitable.xnb_hide_formulaBar = false
153
- bitable.xnb_excel_with_bg = true
154
- bitable.xnb_height = `${Math.floor((window.innerHeight - 100) * 0.72)}px`
155
- bitable.config = bitable.config ?? bitableConfig
156
- }
157
-
158
- function dataColumnsMismatch (bitable: Record<string, unknown>, cols: Array<Record<string, unknown>>): boolean {
159
- const dc = (bitable.data_columns as Array<{ field?: string }> | undefined) ?? []
160
- if (dc.length !== cols.length) return true
161
- return cols.some((c, i) => c.field !== dc[i]?.field)
162
- }
163
-
164
- export interface UseSuperTableBitableLifecycleResult {
165
- choreo: Ref<XNBBitableChoreo | null>
166
- lastRowCount: Ref<number>
167
- /** 首屏:ensure 列结构 + 拉数 + 写 items */
168
- bootstrap: () => Promise<void>
169
- /** 与 bitable-list-lifecycle.js 中 refreshTable 一致:列不齐则补建,再拉数,整表替换 items 后 update */
170
- refreshTable: (opts?: { silentNotify?: boolean; skipLoading?: boolean }) => Promise<number>
171
- /** 校验当前子表行:数量列为正数;`skipLoading` 为 true 时不套 loading(供提交前复用);`silentSuccess` 为 true 时通过不弹成功提示 */
172
- checkRows: (opts?: { skipLoading?: boolean; silentSuccess?: boolean }) => Promise<boolean>
173
- /** 先 `checkRows`,再 POST `/api/execution/complete`(不触发整书保存) */
174
- submitRows: () => Promise<void>
175
- }
176
-
177
- /**
178
- * Vue 版「超级表格列表」生命周期。
179
- * 刷新使用全量 `items = mapped`(`applyRowsToBitable(..., { replaceAll: true })`);全量 list 接口须同此写法(见 orbit skill §8.5.2)。
180
- */
181
- export function useSuperTableBitableLifecycle (
182
- getContext: () => XnbSuperCellContext,
183
- getOpenerPayload: () => unknown
184
- ): UseSuperTableBitableLifecycleResult {
185
- const choreo = ref<XNBBitableChoreo | null>(null)
186
- const lastRowCount = ref(0)
187
- /** 最近一次从 AgenticLab `GET /api/ticket/list` 拉到的完整工单,用于提交时与表格合并为 `tickets` */
188
- const lastAgenticRawTickets = ref<Record<string, unknown>[]>([])
189
- const cellUid = computed(() => getContext().cellUid || '')
190
-
191
- let _prevBookPath = ''
192
- let _prevCellUid = ''
193
-
194
- watch(
195
- () => [getContext().bookPath, getContext().cellUid, getContext().XNBBitable] as const,
196
- () => {
197
- const ctx = getContext()
198
- const bp = ctx.bookPath
199
- const cu = ctx.cellUid
200
- if (bp === _prevBookPath && cu === _prevCellUid && choreo.value != null) return
201
- _prevBookPath = bp
202
- _prevCellUid = cu
203
- const Ctor = ctx.XNBBitable
204
- choreo.value = Ctor != null ? new Ctor({ bookPath: bp, cellUid: cu }) : null
205
- },
206
- { immediate: true }
207
- )
208
-
209
- function xnb (): XNBBitableChoreo {
210
- const c = choreo.value
211
- if (c == null) throw new Error('XNBBitable 未注入')
212
- return c
213
- }
214
-
215
- const listSub: ListSubShape = {
216
- index: DEMO_LIST_SUB.index,
217
- name: DEMO_LIST_SUB.name,
218
- row: DEMO_LIST_SUB.row
219
- }
220
-
221
- async function resolveBitableBySub (sub: ListSubShape): Promise<Record<string, unknown> | null> {
222
- const x = xnb()
223
- if (!(await x.getSubCell())) return null
224
- const all = await x.getAllSubBitable(true)
225
- if (sub.index) {
226
- const hit = all.find((b) => b.index === sub.index)
227
- if (hit) return hit
228
- }
229
- if (all.length === 1) return all[0] ?? null
230
- const main = (await x.getSubBitable(true)) as Record<string, unknown> | null
231
- if (main == null) return null
232
- if (!sub.index || main.index === sub.index) return main
233
- return null
234
- }
235
-
236
- async function rebuildSubBitableColumns (
237
- bitable: Record<string, unknown>,
238
- sub: ListSubShape,
239
- cols: Array<Record<string, unknown>>
240
- ): Promise<void> {
241
- const x = xnb()
242
- bitable.items = []
243
- bitable.data_columns = []
244
- await x.addColumns(bitable, cols, [])
245
- handleBitable(bitable, sub)
246
- await x.updateSubBitable([bitable], { isMerge: false, name: DEMO_SUB_TABLE_NAME })
247
- if (typeof x.delay === 'function') await x.delay(300)
248
- }
249
-
250
- async function createEmptyBitable (isMerge = false): Promise<void> {
251
- const x = xnb()
252
- if (await x.getSubCell()) {
253
- const bitable = (await x.getSubBitable(true)) as Record<string, unknown>
254
- bitable.items = []
255
- bitable.data_columns = []
256
- await x.addColumns(bitable, dataColumns, [])
257
- handleBitable(bitable, listSub)
258
- await x.updateSubBitable([bitable], { isMerge, name: DEMO_SUB_TABLE_NAME })
259
- if (typeof x.delay === 'function') await x.delay(300)
260
- return
261
- }
262
- const bitable: Record<string, unknown> = { items: [], data_columns: [] }
263
- handleBitable(bitable, listSub)
264
- await x.addColumns(bitable, dataColumns, [])
265
- try {
266
- await x.createSubBitable([bitable], DEMO_SUB_TABLE_NAME)
267
- } catch {
268
- if (typeof x.delay === 'function') await x.delay(500)
269
- await x.createSubBitable([bitable], DEMO_SUB_TABLE_NAME)
270
- }
271
- }
272
-
273
- async function fetchDemoList (): Promise<DemoListRow[]> {
274
- const payload = getOpenerPayload()
275
- const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
276
- const token = import.meta.env.VITE_DEV_TOKEN || ''
277
- const ids = extractTicketIdsFromPayload(payload)
278
- if (baseUrl !== '' && ids.length > 0) {
279
- const list = await fetchAgenticlabTicketList({
280
- baseUrl,
281
- token,
282
- ticketIds: ids
283
- })
284
- const fromApi = list.map((t) => mapAgenticlabTicketToDemoRow(t)).filter((r): r is DemoListRow => r != null)
285
- if (fromApi.length > 0) {
286
- lastAgenticRawTickets.value = list.map((t) => {
287
- return t != null && typeof t === 'object' ? (t as Record<string, unknown>) : {}
288
- })
289
- return fromApi
290
- }
291
- }
292
- lastAgenticRawTickets.value = []
293
- const fromTicketList = parseTicketListFromOpenerPayload(payload)
294
- if (fromTicketList != null) return fromTicketList
295
- const fromOpener = parseDemoRowsFromOpenerPayload(payload)
296
- if (fromOpener != null) return fromOpener
297
- /* 无 Agentic 列表、无 opener ticketList/demoRows 时不注入占位行 */
298
- return []
299
- }
300
-
301
- function mapRowToItem (row: DemoListRow, i: number): { id: string; fields: Record<string, string> } {
302
- const fields: Record<string, string> = {}
303
- for (const { field, key } of DEMO_LIST_COLS) {
304
- const raw = row[key as keyof DemoListRow]
305
- fields[field] = raw == null ? '' : typeof raw === 'object' ? JSON.stringify(raw) : String(raw)
306
- }
307
- return {
308
- id: String(row.id ?? `${cellUid.value}-${i}`),
309
- fields
310
- }
311
- }
312
-
313
- /**
314
- * `replaceAll: true`(刷新列表):整表 `items = mapped`,与全量 list 接口写法一致,宿主表格易重绘。
315
- * 默认 `replaceAll: false`:无可见行时整表替换,有行时按 id 合并 fields(询价式增量示例)。
316
- */
317
- async function applyRowsToBitable (
318
- bitable: Record<string, unknown>,
319
- rows: DemoListRow[],
320
- opts?: { replaceAll?: boolean }
321
- ): Promise<void> {
322
- const x = xnb()
323
- const mapped = rows.map((row, i) => mapRowToItem(row, i))
324
- const items = (bitable.items as Array<{ id: string; fields: Record<string, string>; visible?: boolean }> | undefined) ?? []
325
- const visible = items.filter((it) => it?.visible !== false)
326
- if (opts?.replaceAll === true || !visible.length) {
327
- bitable.items = mapped
328
- } else {
329
- const byId = new Map(mapped.map((m) => [m.id, m]))
330
- for (const item of items) {
331
- const hit = byId.get(item.id)
332
- if (hit != null) item.fields = { ...item.fields, ...hit.fields }
333
- }
334
- const have = new Set(items.map((it) => it.id))
335
- for (const m of mapped) {
336
- if (!have.has(m.id)) items.push(m)
337
- }
338
- bitable.items = items
339
- }
340
-
341
- await x.updateSubBitable([bitable], { isMerge: true, name: DEMO_SUB_TABLE_NAME })
342
- if (typeof x.delay === 'function') await x.delay(300)
343
- }
344
-
345
- async function ensureBitableStructure (): Promise<void> {
346
- const x = xnb()
347
- if (!(await x.getSubCell())) {
348
- await createEmptyBitable()
349
- return
350
- }
351
- const bitable = await resolveBitableBySub(listSub)
352
- if (bitable == null) throw new Error('无法解析子表:请核对 listSub.index / name 与 Book')
353
- if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
354
- await rebuildSubBitableColumns(bitable, listSub, dataColumns)
355
- }
356
- }
357
-
358
- async function refreshTable (opts: { silentNotify?: boolean; skipLoading?: boolean } = {}): Promise<number> {
359
- const { silentNotify, skipLoading } = opts
360
- const ctx = getContext()
361
- const loadingShow = ctx.loadingShow
362
- const loadingHide = ctx.loadingHide
363
- const notifySuccess = ctx.notifySuccess
364
- const notifyError = ctx.notifyError
365
- const outer = !skipLoading
366
- if (outer) loadingShow?.()
367
- try {
368
- const x = xnb()
369
- let bitable = await resolveBitableBySub(listSub)
370
- if (bitable == null && (await x.getSubCell())) {
371
- bitable = (await x.getSubBitable(true)) as Record<string, unknown>
372
- }
373
- if (bitable == null) throw new Error('未解析到 bitable')
374
-
375
- if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
376
- await rebuildSubBitableColumns(bitable, listSub, dataColumns)
377
- bitable = (await resolveBitableBySub(listSub)) ?? bitable
378
- }
379
-
380
- const rows = await fetchDemoList()
381
- await applyRowsToBitable(bitable, rows, { replaceAll: true })
382
- lastRowCount.value = rows.length
383
- if (!silentNotify) notifySuccess?.(`已刷新,共 ${rows.length} 条`)
384
- return rows.length
385
- } catch (e) {
386
- notifyError?.(e, '', '刷新失败')
387
- throw e
388
- } finally {
389
- if (outer) loadingHide?.()
390
- }
391
- }
392
-
393
- async function bootstrap (): Promise<void> {
394
- const ctx = getContext()
395
- const loadingShow = ctx.loadingShow
396
- const loadingHide = ctx.loadingHide
397
- const notifySuccess = ctx.notifySuccess
398
- const notifyError = ctx.notifyError
399
- loadingShow?.()
400
- try {
401
- await ensureBitableStructure()
402
- await refreshTable({ silentNotify: true, skipLoading: true })
403
- notifySuccess?.('演示表已加载')
404
- } catch (e) {
405
- notifyError?.(e, '', '首屏加载失败')
406
- throw e
407
- } finally {
408
- loadingHide?.()
409
- }
410
- }
411
-
412
- async function checkRows (opts: { skipLoading?: boolean; silentSuccess?: boolean } = {}): Promise<boolean> {
413
- const { skipLoading = false, silentSuccess = false } = opts
414
- const ctx = getContext()
415
- const notifySuccess = ctx.notifySuccess
416
- const notifyError = ctx.notifyError
417
- const loadingShow = ctx.loadingShow
418
- const loadingHide = ctx.loadingHide
419
- const outer = !skipLoading
420
- if (outer) loadingShow?.()
421
- try {
422
- const x = xnb()
423
- const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
424
- const items = (bitable.items as Array<{ fields?: Record<string, string> }> | undefined) ?? []
425
- const visible = items.filter((it) => (it as { visible?: boolean }).visible !== false)
426
- const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
427
- const bad: string[] = []
428
- for (const it of visible) {
429
- const raw = it.fields?.[qtyField]
430
- const n = Number(raw)
431
- if (!Number.isFinite(n) || n <= 0) bad.push(String(it.fields?.['编码'] ?? it.fields?.['名称'] ?? '?'))
432
- }
433
- if (bad.length) {
434
- notifyError?.(new Error(`数量须为正数,行:${bad.join(', ')}`), '', '数据检查未通过')
435
- return false
436
- }
437
- if (!silentSuccess) {
438
- notifySuccess?.(`数据检查通过,共 ${visible.length} 行`)
439
- }
440
- return true
441
- } catch (e) {
442
- notifyError?.(e, '', '数据检查失败')
443
- throw e
444
- } finally {
445
- if (outer) loadingHide?.()
446
- }
447
- }
448
-
449
- function resolveValveIdForSubmit (): number {
450
- const fromPayload = extractValveIdFromPayload(getOpenerPayload())
451
- if (fromPayload != null) return fromPayload
452
- const env = import.meta.env.VITE_AGENTICLAB_VALVE_ID
453
- if (env != null && env !== '') {
454
- const n = Number(env)
455
- if (Number.isFinite(n)) return n
456
- }
457
- return 0
458
- }
459
-
460
- /** 将表格行中的「数量」写回工单 `detail`(与 `mapAgenticlabTicketToDemoRow` 读取路径对齐) */
461
- function mergeEditedRowIntoAgenticTicket (ticket: Record<string, unknown>, row: DemoListRow): Record<string, unknown> {
462
- const out = JSON.parse(JSON.stringify(ticket)) as Record<string, unknown>
463
- const detail = (out.detail != null && typeof out.detail === 'object' ? out.detail : {}) as Record<string, unknown>
464
- const pp =
465
- detail.process_params != null && typeof detail.process_params === 'object'
466
- ? ({ ...(detail.process_params as Record<string, unknown>) } as Record<string, unknown>)
467
- : ({} as Record<string, unknown>)
468
- pp['@requested_amount_mg'] = row.qty
469
- detail.process_params = pp
470
- detail.target_amount_1_mg = row.qty
471
- out.detail = detail
472
- return out
473
- }
474
-
475
- function buildTicketsForComplete (bitable: Record<string, unknown>): Record<string, unknown>[] {
476
- const rows = itemsToRows(bitable)
477
- const byId = new Map(lastAgenticRawTickets.value.map((t) => [String(t.id), t]))
478
- const tickets: Record<string, unknown>[] = []
479
- for (const row of rows) {
480
- const raw = byId.get(String(row.id))
481
- if (raw == null) continue
482
- tickets.push(mergeEditedRowIntoAgenticTicket(raw, row))
483
- }
484
- return tickets
485
- }
486
-
487
- function itemsToRows (bitable: Record<string, unknown>): DemoListRow[] {
488
- const items = (bitable.items as Array<{ id: string; fields?: Record<string, string> }> | undefined) ?? []
489
- const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
490
- return items
491
- .filter((it) => (it as { visible?: boolean }).visible !== false)
492
- .map((it) => {
493
- const f = it.fields ?? {}
494
- return {
495
- id: String(it.id),
496
- code: f['编码'] ?? '',
497
- name: f['名称'] ?? '',
498
- qty: Number(f[qtyField])
499
- }
500
- })
501
- }
502
-
503
- async function submitRows (): Promise<void> {
504
- const ctx = getContext()
505
- const notifySuccess = ctx.notifySuccess
506
- const notifyError = ctx.notifyError
507
- const loadingShow = ctx.loadingShow
508
- const loadingHide = ctx.loadingHide
509
- loadingShow?.()
510
- try {
511
- if (!(await checkRows({ skipLoading: true, silentSuccess: true }))) {
512
- return
513
- }
514
- const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
515
- const token = import.meta.env.VITE_DEV_TOKEN || ''
516
- if (baseUrl === '') {
517
- notifyError?.(new Error('未配置 VITE_AGENTICLAB_API_URL(接口地址)'), '', '无法提交')
518
- return
519
- }
520
- const x = xnb()
521
- const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
522
- const tickets = buildTicketsForComplete(bitable)
523
- if (tickets.length === 0) {
524
- notifyError?.(
525
- new Error('当前表格行无法对应到已拉取的工单;请通过 ticket_ids 拉取列表后再提交'),
526
- '',
527
- '提交失败'
528
- )
529
- return
530
- }
531
- const valveId = resolveValveIdForSubmit()
532
- await postAgenticlabExecutionComplete({
533
- baseUrl,
534
- token,
535
- valveId,
536
- tickets
537
- })
538
- notifySuccess?.(`已提交 ${tickets.length} 条工单(execution/complete)`)
539
- } catch (e) {
540
- notifyError?.(e, '', '提交失败')
541
- throw e
542
- } finally {
543
- loadingHide?.()
544
- }
545
- }
546
-
547
- return {
548
- choreo,
549
- lastRowCount,
550
- bootstrap,
551
- refreshTable,
552
- checkRows,
553
- submitRows
554
- }
555
- }
1
+ /* eslint-disable @typescript-eslint/typedef, prefer-destructuring, no-continue, quote-props, operator-linebreak */
2
+ import { computed, ref, watch } from 'vue'
3
+ import type { Ref } from 'vue'
4
+ import { fetchAgenticlabTicketList, postAgenticlabExecutionComplete } from '@/api/agenticlabTickets'
5
+ import type { XNBBitableChoreo, XnbSuperCellContext } from '@/types/xnb-context'
6
+ import { extractTicketIdsFromPayload, extractValveIdFromPayload } from '@/utils/openerTicketIds'
7
+
8
+ /** 与 Book 子表、updateSubBitable 最后一参 `name` 对齐(示例名) */
9
+ export const DEMO_SUB_TABLE_NAME = '演示'
10
+
11
+ /** index 须与子表编辑器一致;单主表且编辑器为空时可试 `''`(多子表勿用空串) */
12
+ export const DEMO_LIST_SUB = { index: 'sheet-demo', name: DEMO_SUB_TABLE_NAME, row: 200 } as const
13
+
14
+ const HEADER_GRAY = '#9e9e9e'
15
+
16
+ /** 列定义:field 为表格中文列头,key 为接口/内存行字段 */
17
+ export const DEMO_LIST_COLS = [
18
+ { field: '编码', key: 'code' },
19
+ { field: '名称', key: 'name' },
20
+ { field: '数量', key: 'qty' }
21
+ ] as const
22
+
23
+ export interface DemoListRow {
24
+ id: string
25
+ code: string
26
+ name: string
27
+ qty: number
28
+ }
29
+
30
+ const bitableConfig = {
31
+ columnlen: { '0': 120, '1': 160, '2': 100 },
32
+ rowlen: { 0: 34 }
33
+ }
34
+
35
+ function buildDataColumns (): Array<Record<string, unknown>> {
36
+ const cols: Array<Record<string, unknown>> = [
37
+ {
38
+ w: 120,
39
+ dv: { type: 'text_length' },
40
+ field: '编码',
41
+ readonly: true,
42
+ visible: true,
43
+ dbRelated: false,
44
+ filters: null,
45
+ configurable: false,
46
+ headerColor: HEADER_GRAY
47
+ },
48
+ {
49
+ w: 160,
50
+ dv: { type: 'text_length' },
51
+ field: '名称',
52
+ readonly: true,
53
+ visible: true,
54
+ dbRelated: false,
55
+ filters: null,
56
+ configurable: false,
57
+ headerColor: HEADER_GRAY
58
+ },
59
+ {
60
+ w: 100,
61
+ dv: { type: 'number' },
62
+ ct: { fa: '0', t: 'n' },
63
+ field: '数量',
64
+ readonly: true,
65
+ visible: true,
66
+ dbRelated: false,
67
+ filters: null,
68
+ configurable: false,
69
+ headerColor: HEADER_GRAY
70
+ }
71
+ ]
72
+ cols.forEach((dc, i) => {
73
+ dc.c = i
74
+ })
75
+ return cols
76
+ }
77
+
78
+ const dataColumns = buildDataColumns()
79
+
80
+ /** opener `payload` 中带 `demoRows` 时用于首屏/刷新数据源(类型宽松便于对接) */
81
+ export function parseDemoRowsFromOpenerPayload (payload: unknown): DemoListRow[] | null {
82
+ if (payload == null || typeof payload !== 'object') return null
83
+ const rows = (payload as { demoRows?: unknown }).demoRows
84
+ if (!Array.isArray(rows) || rows.length === 0) return null
85
+ const out: DemoListRow[] = []
86
+ for (const r of rows) {
87
+ if (r == null || typeof r !== 'object') continue
88
+ const o = r as Record<string, unknown>
89
+ const id = o.id != null ? String(o.id) : ''
90
+ if (!id) continue
91
+ const code = o.code != null ? String(o.code) : ''
92
+ const name = o.name != null ? String(o.name) : ''
93
+ const qty = typeof o.qty === 'number' ? o.qty : Number(o.qty)
94
+ if (!Number.isFinite(qty)) continue
95
+ out.push({ id, code, name, qty })
96
+ }
97
+ return out.length ? out : null
98
+ }
99
+
100
+ /** opener 已预拉工单列表时(如父级合并的 `ticketList`),直接映射为表格行 */
101
+ function parseTicketListFromOpenerPayload (payload: unknown): DemoListRow[] | null {
102
+ if (payload == null || typeof payload !== 'object') return null
103
+ const list = (payload as { ticketList?: unknown }).ticketList
104
+ if (!Array.isArray(list) || list.length === 0) return null
105
+ const rows = list.map((t) => mapAgenticlabTicketToDemoRow(t)).filter((r): r is DemoListRow => r != null)
106
+ return rows.length ? rows : null
107
+ }
108
+
109
+ function numOr0 (v: unknown): number {
110
+ const n = typeof v === 'number' ? v : Number(v)
111
+ return Number.isFinite(n) ? n : 0
112
+ }
113
+
114
+ /** 将 `/api/ticket/list` 单项映射为示例表列(编码/名称/数量) */
115
+ function mapAgenticlabTicketToDemoRow (raw: unknown): DemoListRow | null {
116
+ if (raw == null || typeof raw !== 'object') return null
117
+ const t = raw as Record<string, unknown>
118
+ const id = t.id != null ? String(t.id) : ''
119
+ if (!id) return null
120
+ const detail = t.detail != null && typeof t.detail === 'object' ? (t.detail as Record<string, unknown>) : {}
121
+ const ticketId = detail.ticket_id != null ? String(detail.ticket_id) : id
122
+ const name =
123
+ detail.compound_id != null
124
+ ? String(detail.compound_id)
125
+ : detail.smiles != null
126
+ ? String(detail.smiles)
127
+ : String(t.uuid ?? ticketId)
128
+ const pp = detail.process_params != null && typeof detail.process_params === 'object'
129
+ ? (detail.process_params as Record<string, unknown>)
130
+ : {}
131
+ const qty =
132
+ numOr0(pp['@requested_amount_mg']) ||
133
+ numOr0(detail.target_amount_1_mg) ||
134
+ numOr0(detail.target_amount_2_mg) ||
135
+ 0
136
+ return { id, code: ticketId, name, qty }
137
+ }
138
+
139
+ export interface ListSubShape {
140
+ index: string
141
+ name: string
142
+ row: number
143
+ }
144
+
145
+ function handleBitable (bitable: Record<string, unknown>, sub: ListSubShape | undefined): void {
146
+ bitable.name = sub?.name
147
+ if (sub && Object.prototype.hasOwnProperty.call(sub, 'index')) bitable.index = sub.index
148
+ bitable.row = sub?.row ?? 200
149
+ bitable.xnb_show_sheetbar = true
150
+ bitable.xnb_hide_statisticbar = true
151
+ bitable.xnb_hide_toolbar = false
152
+ bitable.xnb_hide_formulaBar = false
153
+ bitable.xnb_excel_with_bg = true
154
+ bitable.xnb_height = `${Math.floor((window.innerHeight - 100) * 0.72)}px`
155
+ bitable.config = bitable.config ?? bitableConfig
156
+ }
157
+
158
+ function dataColumnsMismatch (bitable: Record<string, unknown>, cols: Array<Record<string, unknown>>): boolean {
159
+ const dc = (bitable.data_columns as Array<{ field?: string }> | undefined) ?? []
160
+ if (dc.length !== cols.length) return true
161
+ return cols.some((c, i) => c.field !== dc[i]?.field)
162
+ }
163
+
164
+ export interface UseSuperTableBitableLifecycleResult {
165
+ choreo: Ref<XNBBitableChoreo | null>
166
+ lastRowCount: Ref<number>
167
+ /** 首屏:ensure 列结构 + 拉数 + 写 items */
168
+ bootstrap: () => Promise<void>
169
+ /** 与 bitable-list-lifecycle.js 中 refreshTable 一致:列不齐则补建,再拉数,整表替换 items 后 update */
170
+ refreshTable: (opts?: { silentNotify?: boolean; skipLoading?: boolean }) => Promise<number>
171
+ /** 校验当前子表行:数量列为正数;`skipLoading` 为 true 时不套 loading(供提交前复用);`silentSuccess` 为 true 时通过不弹成功提示 */
172
+ checkRows: (opts?: { skipLoading?: boolean; silentSuccess?: boolean }) => Promise<boolean>
173
+ /** 先 `checkRows`,再 POST `/api/execution/complete`(不触发整书保存) */
174
+ submitRows: () => Promise<void>
175
+ }
176
+
177
+ /**
178
+ * Vue 版「超级表格列表」生命周期。
179
+ * 刷新使用全量 `items = mapped`(`applyRowsToBitable(..., { replaceAll: true })`);全量 list 接口须同此写法(见 orbit skill §8.5.2)。
180
+ */
181
+ export function useSuperTableBitableLifecycle (
182
+ getContext: () => XnbSuperCellContext,
183
+ getOpenerPayload: () => unknown
184
+ ): UseSuperTableBitableLifecycleResult {
185
+ const choreo = ref<XNBBitableChoreo | null>(null)
186
+ const lastRowCount = ref(0)
187
+ /** 最近一次从 AgenticLab `GET /api/ticket/list` 拉到的完整工单,用于提交时与表格合并为 `tickets` */
188
+ const lastAgenticRawTickets = ref<Record<string, unknown>[]>([])
189
+ const cellUid = computed(() => getContext().cellUid || '')
190
+
191
+ let _prevBookPath = ''
192
+ let _prevCellUid = ''
193
+
194
+ watch(
195
+ () => [getContext().bookPath, getContext().cellUid, getContext().XNBBitable] as const,
196
+ () => {
197
+ const ctx = getContext()
198
+ const bp = ctx.bookPath
199
+ const cu = ctx.cellUid
200
+ if (bp === _prevBookPath && cu === _prevCellUid && choreo.value != null) return
201
+ _prevBookPath = bp
202
+ _prevCellUid = cu
203
+ const Ctor = ctx.XNBBitable
204
+ choreo.value = Ctor != null ? new Ctor({ bookPath: bp, cellUid: cu }) : null
205
+ },
206
+ { immediate: true }
207
+ )
208
+
209
+ function xnb (): XNBBitableChoreo {
210
+ const c = choreo.value
211
+ if (c == null) throw new Error('XNBBitable 未注入')
212
+ return c
213
+ }
214
+
215
+ const listSub: ListSubShape = {
216
+ index: DEMO_LIST_SUB.index,
217
+ name: DEMO_LIST_SUB.name,
218
+ row: DEMO_LIST_SUB.row
219
+ }
220
+
221
+ async function resolveBitableBySub (sub: ListSubShape): Promise<Record<string, unknown> | null> {
222
+ const x = xnb()
223
+ if (!(await x.getSubCell())) return null
224
+ const all = await x.getAllSubBitable(true)
225
+ if (sub.index) {
226
+ const hit = all.find((b) => b.index === sub.index)
227
+ if (hit) return hit
228
+ }
229
+ if (all.length === 1) return all[0] ?? null
230
+ const main = (await x.getSubBitable(true)) as Record<string, unknown> | null
231
+ if (main == null) return null
232
+ if (!sub.index || main.index === sub.index) return main
233
+ return null
234
+ }
235
+
236
+ async function rebuildSubBitableColumns (
237
+ bitable: Record<string, unknown>,
238
+ sub: ListSubShape,
239
+ cols: Array<Record<string, unknown>>
240
+ ): Promise<void> {
241
+ const x = xnb()
242
+ bitable.items = []
243
+ bitable.data_columns = []
244
+ await x.addColumns(bitable, cols, [])
245
+ handleBitable(bitable, sub)
246
+ await x.updateSubBitable([bitable], { isMerge: false, name: DEMO_SUB_TABLE_NAME })
247
+ if (typeof x.delay === 'function') await x.delay(300)
248
+ }
249
+
250
+ async function createEmptyBitable (isMerge = false): Promise<void> {
251
+ const x = xnb()
252
+ if (await x.getSubCell()) {
253
+ const bitable = (await x.getSubBitable(true)) as Record<string, unknown>
254
+ bitable.items = []
255
+ bitable.data_columns = []
256
+ await x.addColumns(bitable, dataColumns, [])
257
+ handleBitable(bitable, listSub)
258
+ await x.updateSubBitable([bitable], { isMerge, name: DEMO_SUB_TABLE_NAME })
259
+ if (typeof x.delay === 'function') await x.delay(300)
260
+ return
261
+ }
262
+ const bitable: Record<string, unknown> = { items: [], data_columns: [] }
263
+ handleBitable(bitable, listSub)
264
+ await x.addColumns(bitable, dataColumns, [])
265
+ try {
266
+ await x.createSubBitable([bitable], DEMO_SUB_TABLE_NAME)
267
+ } catch {
268
+ if (typeof x.delay === 'function') await x.delay(500)
269
+ await x.createSubBitable([bitable], DEMO_SUB_TABLE_NAME)
270
+ }
271
+ }
272
+
273
+ async function fetchDemoList (): Promise<DemoListRow[]> {
274
+ const payload = getOpenerPayload()
275
+ const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
276
+ const token = import.meta.env.VITE_DEV_TOKEN || ''
277
+ const ids = extractTicketIdsFromPayload(payload)
278
+ if (baseUrl !== '' && ids.length > 0) {
279
+ const list = await fetchAgenticlabTicketList({
280
+ baseUrl,
281
+ token,
282
+ ticketIds: ids
283
+ })
284
+ const fromApi = list.map((t) => mapAgenticlabTicketToDemoRow(t)).filter((r): r is DemoListRow => r != null)
285
+ if (fromApi.length > 0) {
286
+ lastAgenticRawTickets.value = list.map((t) => {
287
+ return t != null && typeof t === 'object' ? (t as Record<string, unknown>) : {}
288
+ })
289
+ return fromApi
290
+ }
291
+ }
292
+ lastAgenticRawTickets.value = []
293
+ const fromTicketList = parseTicketListFromOpenerPayload(payload)
294
+ if (fromTicketList != null) return fromTicketList
295
+ const fromOpener = parseDemoRowsFromOpenerPayload(payload)
296
+ if (fromOpener != null) return fromOpener
297
+ /* 无 Agentic 列表、无 opener ticketList/demoRows 时不注入占位行 */
298
+ return []
299
+ }
300
+
301
+ function mapRowToItem (row: DemoListRow, i: number): { id: string; fields: Record<string, string> } {
302
+ const fields: Record<string, string> = {}
303
+ for (const { field, key } of DEMO_LIST_COLS) {
304
+ const raw = row[key as keyof DemoListRow]
305
+ fields[field] = raw == null ? '' : typeof raw === 'object' ? JSON.stringify(raw) : String(raw)
306
+ }
307
+ return {
308
+ id: String(row.id ?? `${cellUid.value}-${i}`),
309
+ fields
310
+ }
311
+ }
312
+
313
+ /**
314
+ * `replaceAll: true`(刷新列表):整表 `items = mapped`,与全量 list 接口写法一致,宿主表格易重绘。
315
+ * 默认 `replaceAll: false`:无可见行时整表替换,有行时按 id 合并 fields(询价式增量示例)。
316
+ */
317
+ async function applyRowsToBitable (
318
+ bitable: Record<string, unknown>,
319
+ rows: DemoListRow[],
320
+ opts?: { replaceAll?: boolean }
321
+ ): Promise<void> {
322
+ const x = xnb()
323
+ const mapped = rows.map((row, i) => mapRowToItem(row, i))
324
+ const items = (bitable.items as Array<{ id: string; fields: Record<string, string>; visible?: boolean }> | undefined) ?? []
325
+ const visible = items.filter((it) => it?.visible !== false)
326
+ if (opts?.replaceAll === true || !visible.length) {
327
+ bitable.items = mapped
328
+ } else {
329
+ const byId = new Map(mapped.map((m) => [m.id, m]))
330
+ for (const item of items) {
331
+ const hit = byId.get(item.id)
332
+ if (hit != null) item.fields = { ...item.fields, ...hit.fields }
333
+ }
334
+ const have = new Set(items.map((it) => it.id))
335
+ for (const m of mapped) {
336
+ if (!have.has(m.id)) items.push(m)
337
+ }
338
+ bitable.items = items
339
+ }
340
+
341
+ await x.updateSubBitable([bitable], { isMerge: true, name: DEMO_SUB_TABLE_NAME })
342
+ if (typeof x.delay === 'function') await x.delay(300)
343
+ }
344
+
345
+ async function ensureBitableStructure (): Promise<void> {
346
+ const x = xnb()
347
+ if (!(await x.getSubCell())) {
348
+ await createEmptyBitable()
349
+ return
350
+ }
351
+ const bitable = await resolveBitableBySub(listSub)
352
+ if (bitable == null) throw new Error('无法解析子表:请核对 listSub.index / name 与 Book')
353
+ if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
354
+ await rebuildSubBitableColumns(bitable, listSub, dataColumns)
355
+ }
356
+ }
357
+
358
+ async function refreshTable (opts: { silentNotify?: boolean; skipLoading?: boolean } = {}): Promise<number> {
359
+ const { silentNotify, skipLoading } = opts
360
+ const ctx = getContext()
361
+ const loadingShow = ctx.loadingShow
362
+ const loadingHide = ctx.loadingHide
363
+ const notifySuccess = ctx.notifySuccess
364
+ const notifyError = ctx.notifyError
365
+ const outer = !skipLoading
366
+ if (outer) loadingShow?.()
367
+ try {
368
+ const x = xnb()
369
+ let bitable = await resolveBitableBySub(listSub)
370
+ if (bitable == null && (await x.getSubCell())) {
371
+ bitable = (await x.getSubBitable(true)) as Record<string, unknown>
372
+ }
373
+ if (bitable == null) throw new Error('未解析到 bitable')
374
+
375
+ if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
376
+ await rebuildSubBitableColumns(bitable, listSub, dataColumns)
377
+ bitable = (await resolveBitableBySub(listSub)) ?? bitable
378
+ }
379
+
380
+ const rows = await fetchDemoList()
381
+ await applyRowsToBitable(bitable, rows, { replaceAll: true })
382
+ lastRowCount.value = rows.length
383
+ if (!silentNotify) notifySuccess?.(`已刷新,共 ${rows.length} 条`)
384
+ return rows.length
385
+ } catch (e) {
386
+ notifyError?.(e, '', '刷新失败')
387
+ throw e
388
+ } finally {
389
+ if (outer) loadingHide?.()
390
+ }
391
+ }
392
+
393
+ async function bootstrap (): Promise<void> {
394
+ const ctx = getContext()
395
+ const loadingShow = ctx.loadingShow
396
+ const loadingHide = ctx.loadingHide
397
+ const notifySuccess = ctx.notifySuccess
398
+ const notifyError = ctx.notifyError
399
+ loadingShow?.()
400
+ try {
401
+ await ensureBitableStructure()
402
+ await refreshTable({ silentNotify: true, skipLoading: true })
403
+ notifySuccess?.('演示表已加载')
404
+ } catch (e) {
405
+ notifyError?.(e, '', '首屏加载失败')
406
+ throw e
407
+ } finally {
408
+ loadingHide?.()
409
+ }
410
+ }
411
+
412
+ async function checkRows (opts: { skipLoading?: boolean; silentSuccess?: boolean } = {}): Promise<boolean> {
413
+ const { skipLoading = false, silentSuccess = false } = opts
414
+ const ctx = getContext()
415
+ const notifySuccess = ctx.notifySuccess
416
+ const notifyError = ctx.notifyError
417
+ const loadingShow = ctx.loadingShow
418
+ const loadingHide = ctx.loadingHide
419
+ const outer = !skipLoading
420
+ if (outer) loadingShow?.()
421
+ try {
422
+ const x = xnb()
423
+ const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
424
+ const items = (bitable.items as Array<{ fields?: Record<string, string> }> | undefined) ?? []
425
+ const visible = items.filter((it) => (it as { visible?: boolean }).visible !== false)
426
+ const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
427
+ const bad: string[] = []
428
+ for (const it of visible) {
429
+ const raw = it.fields?.[qtyField]
430
+ const n = Number(raw)
431
+ if (!Number.isFinite(n) || n <= 0) bad.push(String(it.fields?.['编码'] ?? it.fields?.['名称'] ?? '?'))
432
+ }
433
+ if (bad.length) {
434
+ notifyError?.(new Error(`数量须为正数,行:${bad.join(', ')}`), '', '数据检查未通过')
435
+ return false
436
+ }
437
+ if (!silentSuccess) {
438
+ notifySuccess?.(`数据检查通过,共 ${visible.length} 行`)
439
+ }
440
+ return true
441
+ } catch (e) {
442
+ notifyError?.(e, '', '数据检查失败')
443
+ throw e
444
+ } finally {
445
+ if (outer) loadingHide?.()
446
+ }
447
+ }
448
+
449
+ function resolveValveIdForSubmit (): number {
450
+ const fromPayload = extractValveIdFromPayload(getOpenerPayload())
451
+ if (fromPayload != null) return fromPayload
452
+ const env = import.meta.env.VITE_AGENTICLAB_VALVE_ID
453
+ if (env != null && env !== '') {
454
+ const n = Number(env)
455
+ if (Number.isFinite(n)) return n
456
+ }
457
+ return 0
458
+ }
459
+
460
+ /** 将表格行中的「数量」写回工单 `detail`(与 `mapAgenticlabTicketToDemoRow` 读取路径对齐) */
461
+ function mergeEditedRowIntoAgenticTicket (ticket: Record<string, unknown>, row: DemoListRow): Record<string, unknown> {
462
+ const out = JSON.parse(JSON.stringify(ticket)) as Record<string, unknown>
463
+ const detail = (out.detail != null && typeof out.detail === 'object' ? out.detail : {}) as Record<string, unknown>
464
+ const pp =
465
+ detail.process_params != null && typeof detail.process_params === 'object'
466
+ ? ({ ...(detail.process_params as Record<string, unknown>) } as Record<string, unknown>)
467
+ : ({} as Record<string, unknown>)
468
+ pp['@requested_amount_mg'] = row.qty
469
+ detail.process_params = pp
470
+ detail.target_amount_1_mg = row.qty
471
+ out.detail = detail
472
+ return out
473
+ }
474
+
475
+ function buildTicketsForComplete (bitable: Record<string, unknown>): Record<string, unknown>[] {
476
+ const rows = itemsToRows(bitable)
477
+ const byId = new Map(lastAgenticRawTickets.value.map((t) => [String(t.id), t]))
478
+ const tickets: Record<string, unknown>[] = []
479
+ for (const row of rows) {
480
+ const raw = byId.get(String(row.id))
481
+ if (raw == null) continue
482
+ tickets.push(mergeEditedRowIntoAgenticTicket(raw, row))
483
+ }
484
+ return tickets
485
+ }
486
+
487
+ function itemsToRows (bitable: Record<string, unknown>): DemoListRow[] {
488
+ const items = (bitable.items as Array<{ id: string; fields?: Record<string, string> }> | undefined) ?? []
489
+ const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
490
+ return items
491
+ .filter((it) => (it as { visible?: boolean }).visible !== false)
492
+ .map((it) => {
493
+ const f = it.fields ?? {}
494
+ return {
495
+ id: String(it.id),
496
+ code: f['编码'] ?? '',
497
+ name: f['名称'] ?? '',
498
+ qty: Number(f[qtyField])
499
+ }
500
+ })
501
+ }
502
+
503
+ async function submitRows (): Promise<void> {
504
+ const ctx = getContext()
505
+ const notifySuccess = ctx.notifySuccess
506
+ const notifyError = ctx.notifyError
507
+ const loadingShow = ctx.loadingShow
508
+ const loadingHide = ctx.loadingHide
509
+ loadingShow?.()
510
+ try {
511
+ if (!(await checkRows({ skipLoading: true, silentSuccess: true }))) {
512
+ return
513
+ }
514
+ const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
515
+ const token = import.meta.env.VITE_DEV_TOKEN || ''
516
+ if (baseUrl === '') {
517
+ notifyError?.(new Error('未配置 VITE_AGENTICLAB_API_URL(接口地址)'), '', '无法提交')
518
+ return
519
+ }
520
+ const x = xnb()
521
+ const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
522
+ const tickets = buildTicketsForComplete(bitable)
523
+ if (tickets.length === 0) {
524
+ notifyError?.(
525
+ new Error('当前表格行无法对应到已拉取的工单;请通过 ticket_ids 拉取列表后再提交'),
526
+ '',
527
+ '提交失败'
528
+ )
529
+ return
530
+ }
531
+ const valveId = resolveValveIdForSubmit()
532
+ await postAgenticlabExecutionComplete({
533
+ baseUrl,
534
+ token,
535
+ valveId,
536
+ tickets
537
+ })
538
+ notifySuccess?.(`已提交 ${tickets.length} 条工单(execution/complete)`)
539
+ } catch (e) {
540
+ notifyError?.(e, '', '提交失败')
541
+ throw e
542
+ } finally {
543
+ loadingHide?.()
544
+ }
545
+ }
546
+
547
+ return {
548
+ choreo,
549
+ lastRowCount,
550
+ bootstrap,
551
+ refreshTable,
552
+ checkRows,
553
+ submitRows
554
+ }
555
+ }