@xtalpi/agentic-lab-skills 0.0.1

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 (57) hide show
  1. package/README.md +1 -0
  2. package/package.json +14 -0
  3. package/skills/lab-flow-designer/SKILL.md +360 -0
  4. package/skills/lab-flow-designer/embedded-template/SKILL.md +84 -0
  5. package/skills/lab-flow-designer/embedded-template/pools//345/205/245/345/217/243/346/261/240.md +12 -0
  6. package/skills/lab-flow-designer/embedded-template/pools//345/207/272/345/217/243/346/261/240.md +12 -0
  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 +149 -0
  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 +100 -0
  9. package/skills/lab-flow-designer/references/agentic-lab-processer.md +101 -0
  10. package/skills/lab-flow-designer/references/agentic-lab-sdk.md +440 -0
  11. package/skills/lab-flow-designer/references/rhea-api/README.md +7 -0
  12. package/skills/lab-flow-designer/references/rhea-api/execute_process_batch.md +58 -0
  13. package/skills/lab-flow-designer/references/skill-package-layout.md +199 -0
  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 +208 -0
  15. package/skills/lab-orbit-component-builder/SKILL.md +305 -0
  16. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.env.local.example +27 -0
  17. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.eslintignore +7 -0
  18. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.eslintrc.cjs +88 -0
  19. package/skills/lab-orbit-component-builder/examples/xnb-component-template/.nvmrc +1 -0
  20. package/skills/lab-orbit-component-builder/examples/xnb-component-template/AgenticAppAPI.md +268 -0
  21. package/skills/lab-orbit-component-builder/examples/xnb-component-template/Jenkinsfile +106 -0
  22. package/skills/lab-orbit-component-builder/examples/xnb-component-template/OrbitAPI.md +453 -0
  23. package/skills/lab-orbit-component-builder/examples/xnb-component-template/README.md +176 -0
  24. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/public/index.html +12 -0
  25. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/App.vue +151 -0
  26. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/components/DevOpenerLauncher.vue +143 -0
  27. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/global.d.ts +74 -0
  28. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/main.ts +305 -0
  29. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/mockXNBBitable.ts +119 -0
  30. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/shims-vue.d.ts +6 -0
  31. package/skills/lab-orbit-component-builder/examples/xnb-component-template/dev/src/utils/devOpenerHost.ts +75 -0
  32. package/skills/lab-orbit-component-builder/examples/xnb-component-template/index.html +13 -0
  33. package/skills/lab-orbit-component-builder/examples/xnb-component-template/package.json +57 -0
  34. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/api/agenticlabTickets.ts +110 -0
  35. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/entries/bitable.ts +4 -0
  36. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/entries/custom-page.ts +4 -0
  37. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/index.ts +1 -0
  38. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/styles/orbit-quasar-host.scss +19 -0
  39. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/types/context.ts +15 -0
  40. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/types/xnb-context.ts +70 -0
  41. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useBitablePage.ts +180 -0
  42. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useSuperCellDemo.ts +257 -0
  43. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/use/useSuperTableBitableLifecycle.ts +547 -0
  44. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/openerInitParams.ts +158 -0
  45. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/openerTicketIds.ts +32 -0
  46. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/orbitHttpClient.ts +110 -0
  47. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/utils/request.ts +92 -0
  48. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/views/bitable.vue +65 -0
  49. package/skills/lab-orbit-component-builder/examples/xnb-component-template/src/views/custom-page.vue +140 -0
  50. package/skills/lab-orbit-component-builder/examples/xnb-component-template/tsconfig.json +48 -0
  51. package/skills/lab-orbit-component-builder/examples/xnb-component-template/vite.config.ts +165 -0
  52. package/skills/lab-orbit-component-builder/examples/xnb-component-template/vite.dev.config.ts +51 -0
  53. package/skills/lab-orbit-component-builder/references/flow-document-human-ui.md +65 -0
  54. package/skills/lab-orbit-component-builder/references/pool-schema-to-columns.md +67 -0
  55. package/skills/lab-orbit-component-builder/references/vue-template-checklist.md +113 -0
  56. package/skills/lab-orbit-component-builder/references/xnb-context-vue-props.md +49 -0
  57. package/skills/lab-orbit-component-builder/references/xnbitable-vue-parity.md +32 -0
@@ -0,0 +1,547 @@
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
+ watch(
192
+ () => [getContext().bookPath, getContext().cellUid, getContext().XNBBitable] as const,
193
+ () => {
194
+ const ctx = getContext()
195
+ const Ctor = ctx.XNBBitable
196
+ choreo.value =
197
+ Ctor != null
198
+ ? new Ctor({
199
+ bookPath: ctx.bookPath,
200
+ cellUid: ctx.cellUid
201
+ })
202
+ : null
203
+ },
204
+ { immediate: true }
205
+ )
206
+
207
+ function xnb (): XNBBitableChoreo {
208
+ const c = choreo.value
209
+ if (c == null) throw new Error('XNBBitable 未注入')
210
+ return c
211
+ }
212
+
213
+ const listSub: ListSubShape = {
214
+ index: DEMO_LIST_SUB.index,
215
+ name: DEMO_LIST_SUB.name,
216
+ row: DEMO_LIST_SUB.row
217
+ }
218
+
219
+ async function resolveBitableBySub (sub: ListSubShape): Promise<Record<string, unknown> | null> {
220
+ const x = xnb()
221
+ if (!(await x.getSubCell())) return null
222
+ const all = await x.getAllSubBitable(true)
223
+ if (sub.index) {
224
+ const hit = all.find((b) => b.index === sub.index)
225
+ if (hit) return hit
226
+ }
227
+ if (all.length === 1) return all[0] ?? null
228
+ const main = (await x.getSubBitable(true)) as Record<string, unknown> | null
229
+ if (main == null) return null
230
+ if (!sub.index || main.index === sub.index) return main
231
+ return null
232
+ }
233
+
234
+ async function rebuildSubBitableColumns (
235
+ bitable: Record<string, unknown>,
236
+ sub: ListSubShape,
237
+ cols: Array<Record<string, unknown>>
238
+ ): Promise<void> {
239
+ const x = xnb()
240
+ bitable.items = []
241
+ bitable.data_columns = []
242
+ await x.addColumns(bitable, cols, [])
243
+ handleBitable(bitable, sub)
244
+ await x.updateSubBitable([bitable], { isMerge: false, name: DEMO_SUB_TABLE_NAME })
245
+ if (typeof x.delay === 'function') await x.delay(20)
246
+ }
247
+
248
+ async function createEmptyBitable (isMerge = false): Promise<void> {
249
+ const x = xnb()
250
+ if (await x.getSubCell()) {
251
+ const bitable = (await x.getSubBitable(true)) as Record<string, unknown>
252
+ bitable.items = []
253
+ bitable.data_columns = []
254
+ await x.addColumns(bitable, dataColumns, [])
255
+ handleBitable(bitable, listSub)
256
+ await x.updateSubBitable([bitable], { isMerge, name: DEMO_SUB_TABLE_NAME })
257
+ if (typeof x.delay === 'function') await x.delay(10)
258
+ return
259
+ }
260
+ const bitable: Record<string, unknown> = { items: [], data_columns: [] }
261
+ handleBitable(bitable, listSub)
262
+ await x.addColumns(bitable, dataColumns, [])
263
+ await x.createSubBitable([bitable], DEMO_SUB_TABLE_NAME)
264
+ }
265
+
266
+ async function fetchDemoList (): Promise<DemoListRow[]> {
267
+ const payload = getOpenerPayload()
268
+ const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
269
+ const token = import.meta.env.VITE_DEV_TOKEN || ''
270
+ const ids = extractTicketIdsFromPayload(payload)
271
+ if (baseUrl !== '' && ids.length > 0) {
272
+ const list = await fetchAgenticlabTicketList({
273
+ baseUrl,
274
+ token,
275
+ ticketIds: ids
276
+ })
277
+ const fromApi = list.map((t) => mapAgenticlabTicketToDemoRow(t)).filter((r): r is DemoListRow => r != null)
278
+ if (fromApi.length > 0) {
279
+ lastAgenticRawTickets.value = list.map((t) => {
280
+ return t != null && typeof t === 'object' ? (t as Record<string, unknown>) : {}
281
+ })
282
+ return fromApi
283
+ }
284
+ }
285
+ lastAgenticRawTickets.value = []
286
+ const fromTicketList = parseTicketListFromOpenerPayload(payload)
287
+ if (fromTicketList != null) return fromTicketList
288
+ const fromOpener = parseDemoRowsFromOpenerPayload(payload)
289
+ if (fromOpener != null) return fromOpener
290
+ /* 无 Agentic 列表、无 opener ticketList/demoRows 时不注入占位行 */
291
+ return []
292
+ }
293
+
294
+ function mapRowToItem (row: DemoListRow, i: number): { id: string; fields: Record<string, string> } {
295
+ const fields: Record<string, string> = {}
296
+ for (const { field, key } of DEMO_LIST_COLS) {
297
+ const raw = row[key as keyof DemoListRow]
298
+ fields[field] = raw == null ? '' : typeof raw === 'object' ? JSON.stringify(raw) : String(raw)
299
+ }
300
+ return {
301
+ id: String(row.id ?? `${cellUid.value}-${i}`),
302
+ fields
303
+ }
304
+ }
305
+
306
+ /**
307
+ * `replaceAll: true`(刷新列表):整表 `items = mapped`,与全量 list 接口写法一致,宿主表格易重绘。
308
+ * 默认 `replaceAll: false`:无可见行时整表替换,有行时按 id 合并 fields(询价式增量示例)。
309
+ */
310
+ async function applyRowsToBitable (
311
+ bitable: Record<string, unknown>,
312
+ rows: DemoListRow[],
313
+ opts?: { replaceAll?: boolean }
314
+ ): Promise<void> {
315
+ const x = xnb()
316
+ const mapped = rows.map((row, i) => mapRowToItem(row, i))
317
+ const items = (bitable.items as Array<{ id: string; fields: Record<string, string>; visible?: boolean }> | undefined) ?? []
318
+ const visible = items.filter((it) => it?.visible !== false)
319
+ if (opts?.replaceAll === true || !visible.length) {
320
+ bitable.items = mapped
321
+ } else {
322
+ const byId = new Map(mapped.map((m) => [m.id, m]))
323
+ for (const item of items) {
324
+ const hit = byId.get(item.id)
325
+ if (hit != null) item.fields = { ...item.fields, ...hit.fields }
326
+ }
327
+ const have = new Set(items.map((it) => it.id))
328
+ for (const m of mapped) {
329
+ if (!have.has(m.id)) items.push(m)
330
+ }
331
+ bitable.items = items
332
+ }
333
+
334
+ await x.updateSubBitable([bitable], { isMerge: true, name: DEMO_SUB_TABLE_NAME })
335
+ if (typeof x.delay === 'function') await x.delay(80)
336
+ }
337
+
338
+ async function ensureBitableStructure (): Promise<void> {
339
+ const x = xnb()
340
+ if (!(await x.getSubCell())) {
341
+ await createEmptyBitable()
342
+ return
343
+ }
344
+ const bitable = await resolveBitableBySub(listSub)
345
+ if (bitable == null) throw new Error('无法解析子表:请核对 listSub.index / name 与 Book')
346
+ if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
347
+ await rebuildSubBitableColumns(bitable, listSub, dataColumns)
348
+ }
349
+ }
350
+
351
+ async function refreshTable (opts: { silentNotify?: boolean; skipLoading?: boolean } = {}): Promise<number> {
352
+ const { silentNotify, skipLoading } = opts
353
+ const ctx = getContext()
354
+ const loadingShow = ctx.loadingShow
355
+ const loadingHide = ctx.loadingHide
356
+ const notifySuccess = ctx.notifySuccess
357
+ const notifyError = ctx.notifyError
358
+ const outer = !skipLoading
359
+ if (outer) loadingShow?.()
360
+ try {
361
+ const x = xnb()
362
+ let bitable = await resolveBitableBySub(listSub)
363
+ if (bitable == null && (await x.getSubCell())) {
364
+ bitable = (await x.getSubBitable(true)) as Record<string, unknown>
365
+ }
366
+ if (bitable == null) throw new Error('未解析到 bitable')
367
+
368
+ if (dataColumnsMismatch(bitable, dataColumns) || !((bitable.data_columns as unknown[])?.length)) {
369
+ await rebuildSubBitableColumns(bitable, listSub, dataColumns)
370
+ }
371
+
372
+ const rows = await fetchDemoList()
373
+ await applyRowsToBitable(bitable, rows, { replaceAll: true })
374
+ lastRowCount.value = rows.length
375
+ if (!silentNotify) notifySuccess?.(`已刷新,共 ${rows.length} 条`)
376
+ return rows.length
377
+ } catch (e) {
378
+ notifyError?.(e, '', '刷新失败')
379
+ throw e
380
+ } finally {
381
+ if (outer) loadingHide?.()
382
+ }
383
+ }
384
+
385
+ async function bootstrap (): Promise<void> {
386
+ const ctx = getContext()
387
+ const loadingShow = ctx.loadingShow
388
+ const loadingHide = ctx.loadingHide
389
+ const notifySuccess = ctx.notifySuccess
390
+ const notifyError = ctx.notifyError
391
+ loadingShow?.()
392
+ try {
393
+ await ensureBitableStructure()
394
+ await refreshTable({ silentNotify: true, skipLoading: true })
395
+ notifySuccess?.('演示表已加载')
396
+ } catch (e) {
397
+ notifyError?.(e, '', '首屏加载失败')
398
+ throw e
399
+ } finally {
400
+ loadingHide?.()
401
+ }
402
+ }
403
+
404
+ async function checkRows (opts: { skipLoading?: boolean; silentSuccess?: boolean } = {}): Promise<boolean> {
405
+ const { skipLoading = false, silentSuccess = false } = opts
406
+ const ctx = getContext()
407
+ const notifySuccess = ctx.notifySuccess
408
+ const notifyError = ctx.notifyError
409
+ const loadingShow = ctx.loadingShow
410
+ const loadingHide = ctx.loadingHide
411
+ const outer = !skipLoading
412
+ if (outer) loadingShow?.()
413
+ try {
414
+ const x = xnb()
415
+ const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
416
+ const items = (bitable.items as Array<{ fields?: Record<string, string> }> | undefined) ?? []
417
+ const visible = items.filter((it) => (it as { visible?: boolean }).visible !== false)
418
+ const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
419
+ const bad: string[] = []
420
+ for (const it of visible) {
421
+ const raw = it.fields?.[qtyField]
422
+ const n = Number(raw)
423
+ if (!Number.isFinite(n) || n <= 0) bad.push(String(it.fields?.['编码'] ?? it.fields?.['名称'] ?? '?'))
424
+ }
425
+ if (bad.length) {
426
+ notifyError?.(new Error(`数量须为正数,行:${bad.join(', ')}`), '', '数据检查未通过')
427
+ return false
428
+ }
429
+ if (!silentSuccess) {
430
+ notifySuccess?.(`数据检查通过,共 ${visible.length} 行`)
431
+ }
432
+ return true
433
+ } catch (e) {
434
+ notifyError?.(e, '', '数据检查失败')
435
+ throw e
436
+ } finally {
437
+ if (outer) loadingHide?.()
438
+ }
439
+ }
440
+
441
+ function resolveValveIdForSubmit (): number {
442
+ const fromPayload = extractValveIdFromPayload(getOpenerPayload())
443
+ if (fromPayload != null) return fromPayload
444
+ const env = import.meta.env.VITE_AGENTICLAB_VALVE_ID
445
+ if (env != null && env !== '') {
446
+ const n = Number(env)
447
+ if (Number.isFinite(n)) return n
448
+ }
449
+ return 0
450
+ }
451
+
452
+ /** 将表格行中的「数量」写回工单 `detail`(与 `mapAgenticlabTicketToDemoRow` 读取路径对齐) */
453
+ function mergeEditedRowIntoAgenticTicket (ticket: Record<string, unknown>, row: DemoListRow): Record<string, unknown> {
454
+ const out = JSON.parse(JSON.stringify(ticket)) as Record<string, unknown>
455
+ const detail = (out.detail != null && typeof out.detail === 'object' ? out.detail : {}) as Record<string, unknown>
456
+ const pp =
457
+ detail.process_params != null && typeof detail.process_params === 'object'
458
+ ? ({ ...(detail.process_params as Record<string, unknown>) } as Record<string, unknown>)
459
+ : ({} as Record<string, unknown>)
460
+ pp['@requested_amount_mg'] = row.qty
461
+ detail.process_params = pp
462
+ detail.target_amount_1_mg = row.qty
463
+ out.detail = detail
464
+ return out
465
+ }
466
+
467
+ function buildTicketsForComplete (bitable: Record<string, unknown>): Record<string, unknown>[] {
468
+ const rows = itemsToRows(bitable)
469
+ const byId = new Map(lastAgenticRawTickets.value.map((t) => [String(t.id), t]))
470
+ const tickets: Record<string, unknown>[] = []
471
+ for (const row of rows) {
472
+ const raw = byId.get(String(row.id))
473
+ if (raw == null) continue
474
+ tickets.push(mergeEditedRowIntoAgenticTicket(raw, row))
475
+ }
476
+ return tickets
477
+ }
478
+
479
+ function itemsToRows (bitable: Record<string, unknown>): DemoListRow[] {
480
+ const items = (bitable.items as Array<{ id: string; fields?: Record<string, string> }> | undefined) ?? []
481
+ const qtyField = DEMO_LIST_COLS.find((c) => c.key === 'qty')?.field ?? '数量'
482
+ return items
483
+ .filter((it) => (it as { visible?: boolean }).visible !== false)
484
+ .map((it) => {
485
+ const f = it.fields ?? {}
486
+ return {
487
+ id: String(it.id),
488
+ code: f['编码'] ?? '',
489
+ name: f['名称'] ?? '',
490
+ qty: Number(f[qtyField])
491
+ }
492
+ })
493
+ }
494
+
495
+ async function submitRows (): Promise<void> {
496
+ const ctx = getContext()
497
+ const notifySuccess = ctx.notifySuccess
498
+ const notifyError = ctx.notifyError
499
+ const loadingShow = ctx.loadingShow
500
+ const loadingHide = ctx.loadingHide
501
+ loadingShow?.()
502
+ try {
503
+ if (!(await checkRows({ skipLoading: true, silentSuccess: true }))) {
504
+ return
505
+ }
506
+ const baseUrl = import.meta.env.VITE_AGENTICLAB_API_URL || ''
507
+ const token = import.meta.env.VITE_DEV_TOKEN || ''
508
+ if (baseUrl === '') {
509
+ notifyError?.(new Error('未配置 VITE_AGENTICLAB_API_URL(接口地址)'), '', '无法提交')
510
+ return
511
+ }
512
+ const x = xnb()
513
+ const bitable = (await resolveBitableBySub(listSub)) ?? ((await x.getSubBitable(true)) as Record<string, unknown>)
514
+ const tickets = buildTicketsForComplete(bitable)
515
+ if (tickets.length === 0) {
516
+ notifyError?.(
517
+ new Error('当前表格行无法对应到已拉取的工单;请通过 ticket_ids 拉取列表后再提交'),
518
+ '',
519
+ '提交失败'
520
+ )
521
+ return
522
+ }
523
+ const valveId = resolveValveIdForSubmit()
524
+ await postAgenticlabExecutionComplete({
525
+ baseUrl,
526
+ token,
527
+ valveId,
528
+ tickets
529
+ })
530
+ notifySuccess?.(`已提交 ${tickets.length} 条工单(execution/complete)`)
531
+ } catch (e) {
532
+ notifyError?.(e, '', '提交失败')
533
+ throw e
534
+ } finally {
535
+ loadingHide?.()
536
+ }
537
+ }
538
+
539
+ return {
540
+ choreo,
541
+ lastRowCount,
542
+ bootstrap,
543
+ refreshTable,
544
+ checkRows,
545
+ submitRows
546
+ }
547
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * 弹层 / 子页由 opener 打开时,用 URL `?trace=...` 与 opener `postMessage` 拉取完整初始化参数。
3
+ *
4
+ * **子页发送**:`{ msg: 'get-data', trace }`
5
+ *
6
+ * **宿主回包(流程页标准)**:
7
+ * `{ msg: 'get-data-response', trace: '<同一 trace>', ticket_ids, valve_id, ... }`
8
+ *
9
+ * **兼容旧约定**:`{ msg: 'data', trace, payload: <对象> }` 或 `msg: 'data'` 且字段平铺在根上。
10
+ *
11
+ * 监听器内校验 `e.source === window.opener`(跨源 postMessage 仍适用)。
12
+ *
13
+ * Dev 无 opener 或缺 trace 时返回 `skipped`;development 可读 `VITE_DEV_OPENER_INIT_JSON` 作回退。
14
+ */
15
+
16
+ export type OpenerInitSkipped = 'no-opener-or-closed' | 'no-trace'
17
+
18
+ /** 流程页 / AgenticLab 宿主标准回包 msg */
19
+ export const OPENER_MSG_GET_DATA_RESPONSE = 'get-data-response'
20
+
21
+ /** 模板 / dev 兼容回包 msg */
22
+ export const OPENER_MSG_DATA = 'data'
23
+
24
+ export interface OpenerInitDeps {
25
+ loadingShow: () => void
26
+ loadingHide: () => void
27
+ notifyError: (msg: unknown, ...rest: unknown[]) => void
28
+ /** 等待 opener 回包超时,默认 10000ms */
29
+ timeoutMs?: number
30
+ }
31
+
32
+ export interface OpenerInitResult {
33
+ ok: boolean
34
+ trace: string | null
35
+ /** 规范化后的业务参数(含 ticket_ids、valve_id 等) */
36
+ payload: unknown
37
+ skipped?: OpenerInitSkipped
38
+ /** 为 true 时表示 payload 来自 `VITE_DEV_OPENER_INIT_JSON`(仅 development) */
39
+ devFromEnv?: boolean
40
+ }
41
+
42
+ /**
43
+ * 解析 `.env.local` 中的 `VITE_DEV_OPENER_INIT_JSON`;非 development 或未配置时返回 `null`。
44
+ */
45
+ export function parseDevOpenerInitFromEnv (): unknown | null {
46
+ if (!import.meta.env.DEV) return null
47
+ const raw = import.meta.env.VITE_DEV_OPENER_INIT_JSON
48
+ if (raw == null || typeof raw !== 'string' || raw.trim() === '') return null
49
+ try {
50
+ return JSON.parse(raw) as unknown
51
+ } catch {
52
+ console.warn('[openerInitParams] VITE_DEV_OPENER_INIT_JSON 不是合法 JSON,已忽略')
53
+ return null
54
+ }
55
+ }
56
+
57
+ function devPayloadWhenSkipped (): OpenerInitResult | null {
58
+ const dev = parseDevOpenerInitFromEnv()
59
+ if (dev == null) return null
60
+ return { ok: true, trace: null, payload: dev, devFromEnv: true }
61
+ }
62
+
63
+ /** 部分浏览器下 `e.source` 与缓存的 `opener` 引用不完全相等,需兼容 WindowProxy */
64
+ function isMessageFromOpener (e: MessageEvent, opener: Window): boolean {
65
+ if (e.source == null) return false
66
+ return e.source === opener || e.source === window.opener
67
+ }
68
+
69
+ function omitMsgTrace (d: Record<string, unknown>): Record<string, unknown> {
70
+ const out: Record<string, unknown> = {}
71
+ for (const [k, v] of Object.entries(d)) {
72
+ if (k !== 'msg' && k !== 'trace') {
73
+ out[k] = v
74
+ }
75
+ }
76
+ return out
77
+ }
78
+
79
+ /**
80
+ * 将宿主 postMessage 回包规范为业务 payload(供 `openerTicketIds` 等使用)。
81
+ * @returns 匹配 trace 且 msg 合法时返回对象,否则 `null`
82
+ */
83
+ export function parseOpenerReplyMessage (
84
+ d: Record<string, unknown>,
85
+ trace: string
86
+ ): unknown | null {
87
+ if (String(d.trace) !== String(trace)) return null
88
+
89
+ if (d.msg === OPENER_MSG_GET_DATA_RESPONSE) {
90
+ return omitMsgTrace(d)
91
+ }
92
+
93
+ if (d.msg === OPENER_MSG_DATA) {
94
+ if ('payload' in d && d.payload != null && typeof d.payload === 'object') {
95
+ return d.payload
96
+ }
97
+ const flat = omitMsgTrace(d)
98
+ return Object.keys(flat).length > 0 ? flat : null
99
+ }
100
+
101
+ return null
102
+ }
103
+
104
+ /**
105
+ * 拉取 opener 初始化参数
106
+ */
107
+ export async function fetchOpenerInitPayload (deps: OpenerInitDeps): Promise<OpenerInitResult> {
108
+ const { loadingShow, loadingHide, notifyError, timeoutMs = 120000 } = deps
109
+ loadingShow()
110
+ try {
111
+ const opener = window.opener as Window | null
112
+ if (opener == null || opener.closed) {
113
+ const fromEnv = devPayloadWhenSkipped()
114
+ if (fromEnv != null) return fromEnv
115
+ return { ok: false, trace: null, payload: null, skipped: 'no-opener-or-closed' }
116
+ }
117
+
118
+ const searchParams = new URLSearchParams(window.location.search)
119
+ const trace = searchParams.get('trace')
120
+ if (trace == null || trace === '') {
121
+ const fromEnv = devPayloadWhenSkipped()
122
+ if (fromEnv != null) return fromEnv
123
+ return { ok: false, trace: null, payload: null, skipped: 'no-trace' }
124
+ }
125
+
126
+ const payload = await new Promise<unknown>((resolve: (v: unknown) => void, reject: (e?: unknown) => void) => {
127
+ const timer = window.setTimeout(() => {
128
+ window.removeEventListener('message', onMessage)
129
+ const err = new Error('Timeout')
130
+ notifyError(err, '', '初始化超时')
131
+ reject(err)
132
+ }, timeoutMs)
133
+
134
+ const onMessage = (e: MessageEvent): void => {
135
+ if (!isMessageFromOpener(e, opener)) return
136
+ const d = e.data as Record<string, unknown> | null
137
+ if (d == null || typeof d !== 'object') return
138
+
139
+ const parsed = parseOpenerReplyMessage(d, trace)
140
+ if (parsed == null) return
141
+
142
+ window.clearTimeout(timer)
143
+ window.removeEventListener('message', onMessage)
144
+ resolve(parsed)
145
+ }
146
+
147
+ window.addEventListener('message', onMessage)
148
+ opener.postMessage({ msg: 'get-data', trace }, '*')
149
+ })
150
+
151
+ return { ok: true, trace, payload }
152
+ } catch (err) {
153
+ notifyError(err, '', '初始化失败')
154
+ throw err
155
+ } finally {
156
+ loadingHide()
157
+ }
158
+ }