@gadmin2n/schematics 0.0.87 → 0.0.89
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.
- package/dist/lib/application/files/gadmin2-game-angle-demo/.dockerignore +16 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.codegen +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +76 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.web +53 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/Jenkinsfile +219 -33
- package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +250 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/dev/postgres/init.sql +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +170 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +254 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +8 -7
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/prismaModels.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/audit.seed.ts +40 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +56 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +39 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/{scripts/sync-data-mngt-pages.ts → seed/data-mngt.seed.ts} +36 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/game.seed.ts +44 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +30 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +130 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-event-trigger.ts +60 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +11 -25
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +108 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/main.ts +1 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/agendaJob/agendaJob.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/audit/audit.service.spec.ts +41 -57
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/game/game.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/page/page.service.spec.ts +315 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/pageResource/pageResource.service.spec.ts +312 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/resource/resource.service.spec.ts +317 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/role/role.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/rolePages/rolePages.service.spec.ts +299 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/roleResource/roleResource.service.spec.ts +307 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.controller.spec.ts +31 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/user/user.service.spec.ts +309 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.spec.ts +205 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/dsl-validate.util.ts +116 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.spec.ts +158 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/temporal.service.ts +110 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.spec.ts +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/webhook-signature.util.ts +54 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +34 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +457 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +241 -4
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowEventOutbox/workflowEventOutbox.service.spec.ts +24 -30
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeInstance/workflowNodeInstance.service.spec.ts +36 -36
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.controller.spec.ts +34 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +48 -24
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +312 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +152 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +12 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +79 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +270 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +23 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +70 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +246 -90
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +20 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +96 -8
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/nginx.conf +74 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/ElementInspector.tsx +18 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/helpers/form.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/plugins/devShellPlugin.ts +4 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +9 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +156 -139
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +14 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +62 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/PublishModal.tsx +4 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +18 -27
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasDefaults.ts +32 -11
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/demos.ts +48 -61
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas-page/index.tsx +3 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +16 -16
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +28 -35
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +34 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/styles/antd.css +6 -0
- package/package.json +1 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile +0 -63
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/sync-resources.ts +0 -100
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permissions.ts +0 -302
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.spec.ts +0 -20
- package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/sql/create-event-trigger.sql +0 -87
- /package/dist/lib/application/files/gadmin2-game-angle-demo/{GRACEFUL-DEPLOYMENT.md → server/GRACEFUL-DEPLOYMENT.md} +0 -0
package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { Client as PgClient } from "pg";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
Client,
|
|
4
|
+
Connection,
|
|
5
|
+
WorkflowExecutionAlreadyStartedError,
|
|
6
|
+
} from "@temporalio/client";
|
|
3
7
|
import { config } from "./config";
|
|
4
8
|
|
|
5
9
|
const POLL_INTERVAL = Number(process.env.EVENT_POLL_INTERVAL_MS) || 10_000; // 默认 10 秒
|
|
6
10
|
const BATCH_SIZE = 50;
|
|
7
11
|
const MAX_RETRIES = Number(process.env.OUTBOX_MAX_RETRIES) || 5;
|
|
8
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Outbox Poller 控制句柄。stop() 用于优雅关闭:
|
|
15
|
+
* - 不再调度下一次 poll
|
|
16
|
+
* - 等待当前 in-flight poll 完成
|
|
17
|
+
* - 释放 PG 与 Temporal 连接
|
|
18
|
+
*/
|
|
19
|
+
export interface OutboxPollerHandle {
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
9
23
|
/**
|
|
10
24
|
* Outbox Poller — 定时扫描 t_workflow_event_outbox 未处理事件,匹配并触发 workflow。
|
|
11
25
|
*
|
|
@@ -14,8 +28,14 @@ const MAX_RETRIES = Number(process.env.OUTBOX_MAX_RETRIES) || 5;
|
|
|
14
28
|
* 2. 本轮询器每 N 秒扫描未处理行
|
|
15
29
|
* 3. 匹配 event_trigger workflow → 启动 Temporal 执行
|
|
16
30
|
* 4. 标记行为已处理
|
|
31
|
+
*
|
|
32
|
+
* 事务边界(重要):
|
|
33
|
+
* - 每个 outbox 行用一个独立的 PG 事务包裹:BEGIN → SELECT FOR UPDATE → 触发 workflow → UPDATE processed → COMMIT
|
|
34
|
+
* - workflowId 由 outbox.id 派生为确定性值(`wf-outbox-{id}-w{workflowId}`),
|
|
35
|
+
* 即使在 workflow.start 与 COMMIT 之间崩溃,下次 poll 重试时 Temporal 会抛
|
|
36
|
+
* WorkflowExecutionAlreadyStartedError,被本模块识别为幂等成功,不会丢事件、不会重复启动 workflow。
|
|
17
37
|
*/
|
|
18
|
-
export async function startOutboxPoller() {
|
|
38
|
+
export async function startOutboxPoller(): Promise<OutboxPollerHandle> {
|
|
19
39
|
const pg = new PgClient({ connectionString: config.database.url });
|
|
20
40
|
await pg.connect();
|
|
21
41
|
|
|
@@ -27,68 +47,27 @@ export async function startOutboxPoller() {
|
|
|
27
47
|
namespace: config.temporal.namespace,
|
|
28
48
|
});
|
|
29
49
|
|
|
30
|
-
console.log(
|
|
50
|
+
console.log(
|
|
51
|
+
`[OutboxPoller] Started. Polling every ${POLL_INTERVAL}ms, max retries: ${MAX_RETRIES}`,
|
|
52
|
+
);
|
|
31
53
|
|
|
32
54
|
const poll = async () => {
|
|
33
55
|
try {
|
|
34
|
-
//
|
|
35
|
-
const { rows } = await pg.query(
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
WHERE processed = FALSE
|
|
41
|
-
ORDER BY created_at
|
|
42
|
-
LIMIT $1
|
|
43
|
-
FOR UPDATE SKIP LOCKED
|
|
44
|
-
)
|
|
45
|
-
RETURNING id, event_name, payload, retry_count`,
|
|
56
|
+
// 1. 先 SELECT 候选 id(不锁),减少长事务持锁时间
|
|
57
|
+
const { rows: candidates } = await pg.query(
|
|
58
|
+
`SELECT id FROM t_workflow_event_outbox
|
|
59
|
+
WHERE processed = FALSE
|
|
60
|
+
ORDER BY created_at
|
|
61
|
+
LIMIT $1`,
|
|
46
62
|
[BATCH_SIZE],
|
|
47
63
|
);
|
|
48
64
|
|
|
49
|
-
if (
|
|
50
|
-
console.log(`[OutboxPoller]
|
|
65
|
+
if (candidates.length === 0) return;
|
|
66
|
+
console.log(`[OutboxPoller] Found ${candidates.length} candidate events`);
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
pg,
|
|
56
|
-
temporalClient,
|
|
57
|
-
row.event_name,
|
|
58
|
-
row.payload,
|
|
59
|
-
);
|
|
60
|
-
if (matched === 0) {
|
|
61
|
-
console.warn(
|
|
62
|
-
`[OutboxPoller] No workflow matched event "${row.event_name}" (id=${row.id}). Event consumed without action.`,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
} catch (err: any) {
|
|
66
|
-
const nextRetry = (row.retry_count ?? 0) + 1;
|
|
67
|
-
const errMsg = String(err?.message ?? err);
|
|
68
|
-
if (nextRetry >= MAX_RETRIES) {
|
|
69
|
-
// 终态失败:保持 processed=true,仅记录 retry_count + last_error,不再重试
|
|
70
|
-
console.error(
|
|
71
|
-
`[OutboxPoller] Event ${row.id} (${row.event_name}) failed ${nextRetry} times. Marking as terminal failure: ${errMsg}`,
|
|
72
|
-
);
|
|
73
|
-
await pg.query(
|
|
74
|
-
`UPDATE t_workflow_event_outbox
|
|
75
|
-
SET retry_count = $1, last_error = $2
|
|
76
|
-
WHERE id = $3`,
|
|
77
|
-
[nextRetry, errMsg, row.id],
|
|
78
|
-
);
|
|
79
|
-
} else {
|
|
80
|
-
console.error(
|
|
81
|
-
`[OutboxPoller] Event ${row.id} (${row.event_name}) failed (retry ${nextRetry}/${MAX_RETRIES}): ${errMsg}`,
|
|
82
|
-
);
|
|
83
|
-
// 标记回未处理,下次重试
|
|
84
|
-
await pg.query(
|
|
85
|
-
`UPDATE t_workflow_event_outbox
|
|
86
|
-
SET processed = FALSE, processed_at = NULL, retry_count = $1, last_error = $2
|
|
87
|
-
WHERE id = $3`,
|
|
88
|
-
[nextRetry, errMsg, row.id],
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
68
|
+
// 2. 逐行处理:每行一个独立事务,保证 workflow.start 与 outbox UPDATE 的原子性
|
|
69
|
+
for (const { id } of candidates) {
|
|
70
|
+
await processOneEvent(pg, temporalClient, Number(id));
|
|
92
71
|
}
|
|
93
72
|
} catch (err: any) {
|
|
94
73
|
console.error("[OutboxPoller] Poll error:", err.message);
|
|
@@ -96,24 +75,180 @@ export async function startOutboxPoller() {
|
|
|
96
75
|
};
|
|
97
76
|
|
|
98
77
|
// 自调度循环:每次 poll 完成后再排下一次,避免 setInterval 在 poll 慢于 POLL_INTERVAL 时重叠调用
|
|
78
|
+
let stopped = false;
|
|
79
|
+
let timer: NodeJS.Timeout | null = null;
|
|
80
|
+
let inFlight: Promise<void> | null = null;
|
|
81
|
+
|
|
99
82
|
const loop = async () => {
|
|
83
|
+
if (stopped) return;
|
|
84
|
+
inFlight = poll();
|
|
100
85
|
try {
|
|
101
|
-
await
|
|
86
|
+
await inFlight;
|
|
102
87
|
} finally {
|
|
103
|
-
|
|
88
|
+
inFlight = null;
|
|
89
|
+
if (!stopped) {
|
|
90
|
+
timer = setTimeout(loop, POLL_INTERVAL);
|
|
91
|
+
}
|
|
104
92
|
}
|
|
105
93
|
};
|
|
106
|
-
loop();
|
|
94
|
+
void loop();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
async stop() {
|
|
98
|
+
stopped = true;
|
|
99
|
+
if (timer) {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
timer = null;
|
|
102
|
+
}
|
|
103
|
+
// 等待当前正在执行的 poll 完成(含本轮所有事件处理)
|
|
104
|
+
if (inFlight) {
|
|
105
|
+
try {
|
|
106
|
+
await inFlight;
|
|
107
|
+
} catch {
|
|
108
|
+
// poll 内部已捕获错误并写日志,这里忽略
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
await pg.end();
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
console.error(
|
|
115
|
+
"[OutboxPoller] PG client close error:",
|
|
116
|
+
err?.message ?? err,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await temporalConn.close();
|
|
121
|
+
} catch (err: any) {
|
|
122
|
+
console.error(
|
|
123
|
+
"[OutboxPoller] Temporal connection close error:",
|
|
124
|
+
err?.message ?? err,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
console.log("[OutboxPoller] Stopped, resources released");
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 处理单条 outbox 事件(一个事务)。
|
|
134
|
+
*
|
|
135
|
+
* 流程:
|
|
136
|
+
* BEGIN
|
|
137
|
+
* SELECT FOR UPDATE SKIP LOCKED
|
|
138
|
+
* ↓
|
|
139
|
+
* matchAndTrigger(INSERT instance + workflow.start,workflowId 由 outbox.id 派生)
|
|
140
|
+
* ↓
|
|
141
|
+
* ┌─ 全部成功: UPDATE processed=TRUE → COMMIT
|
|
142
|
+
* ├─ 还能重试: UPDATE retry_count(保持 processed=FALSE)→ COMMIT
|
|
143
|
+
* └─ 终态失败: UPDATE processed=TRUE + last_error → COMMIT
|
|
144
|
+
*/
|
|
145
|
+
async function processOneEvent(
|
|
146
|
+
pg: PgClient,
|
|
147
|
+
temporalClient: Client,
|
|
148
|
+
outboxId: number,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
await pg.query("BEGIN");
|
|
151
|
+
let inTransaction = true;
|
|
152
|
+
try {
|
|
153
|
+
// 行级锁:SKIP LOCKED 保证多实例 poller 不会同时处理同一行
|
|
154
|
+
const { rows } = await pg.query(
|
|
155
|
+
`SELECT id, event_name, payload, retry_count
|
|
156
|
+
FROM t_workflow_event_outbox
|
|
157
|
+
WHERE id = $1 AND processed = FALSE
|
|
158
|
+
FOR UPDATE SKIP LOCKED`,
|
|
159
|
+
[outboxId],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (rows.length === 0) {
|
|
163
|
+
// 已被另一实例处理或已 processed
|
|
164
|
+
await pg.query("ROLLBACK");
|
|
165
|
+
inTransaction = false;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const row = rows[0];
|
|
170
|
+
try {
|
|
171
|
+
const matched = await matchAndTrigger(
|
|
172
|
+
pg,
|
|
173
|
+
temporalClient,
|
|
174
|
+
Number(row.id),
|
|
175
|
+
row.event_name,
|
|
176
|
+
row.payload,
|
|
177
|
+
);
|
|
178
|
+
if (matched === 0) {
|
|
179
|
+
console.warn(
|
|
180
|
+
`[OutboxPoller] No workflow matched event "${row.event_name}" (id=${row.id}). Event consumed without action.`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 全部 workflow.start 成功(含幂等成功),原子提交"已处理"
|
|
185
|
+
await pg.query(
|
|
186
|
+
`UPDATE t_workflow_event_outbox
|
|
187
|
+
SET processed = TRUE, processed_at = NOW()
|
|
188
|
+
WHERE id = $1`,
|
|
189
|
+
[row.id],
|
|
190
|
+
);
|
|
191
|
+
await pg.query("COMMIT");
|
|
192
|
+
inTransaction = false;
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
// workflow.start / DB 操作失败 —— 回滚后用独立事务记录 retry 状态
|
|
195
|
+
await pg.query("ROLLBACK");
|
|
196
|
+
inTransaction = false;
|
|
197
|
+
|
|
198
|
+
const nextRetry = (row.retry_count ?? 0) + 1;
|
|
199
|
+
const errMsg = String(err?.message ?? err);
|
|
200
|
+
|
|
201
|
+
if (nextRetry >= MAX_RETRIES) {
|
|
202
|
+
console.error(
|
|
203
|
+
`[OutboxPoller] Event ${row.id} (${row.event_name}) failed ${nextRetry} times. Marking as terminal failure: ${errMsg}`,
|
|
204
|
+
);
|
|
205
|
+
await pg.query(
|
|
206
|
+
`UPDATE t_workflow_event_outbox
|
|
207
|
+
SET processed = TRUE, processed_at = NOW(), retry_count = $1, last_error = $2
|
|
208
|
+
WHERE id = $3`,
|
|
209
|
+
[nextRetry, errMsg, row.id],
|
|
210
|
+
);
|
|
211
|
+
} else {
|
|
212
|
+
console.error(
|
|
213
|
+
`[OutboxPoller] Event ${row.id} (${row.event_name}) failed (retry ${nextRetry}/${MAX_RETRIES}): ${errMsg}`,
|
|
214
|
+
);
|
|
215
|
+
await pg.query(
|
|
216
|
+
`UPDATE t_workflow_event_outbox
|
|
217
|
+
SET retry_count = $1, last_error = $2
|
|
218
|
+
WHERE id = $3`,
|
|
219
|
+
[nextRetry, errMsg, row.id],
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch (txErr: any) {
|
|
224
|
+
// 事务级异常(PG 连接断等)— 尝试回滚兜底
|
|
225
|
+
if (inTransaction) {
|
|
226
|
+
try {
|
|
227
|
+
await pg.query("ROLLBACK");
|
|
228
|
+
} catch {
|
|
229
|
+
/* ignore */
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.error(
|
|
233
|
+
`[OutboxPoller] Tx error for outbox ${outboxId}:`,
|
|
234
|
+
txErr?.message ?? txErr,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
107
237
|
}
|
|
108
238
|
|
|
109
239
|
/**
|
|
110
240
|
* 查找监听该 eventName 的 workflow 并启动执行。
|
|
111
241
|
*
|
|
112
|
-
*
|
|
242
|
+
* **幂等性**:workflowId 由 outboxId + workflow_id 派生为确定性值。
|
|
243
|
+
* 若先前 poll 已成功 start 但事务未 COMMIT 就崩溃,本次重试时
|
|
244
|
+
* Temporal 会抛 WorkflowExecutionAlreadyStartedError —— 视为幂等成功。
|
|
245
|
+
*
|
|
246
|
+
* @returns 实际匹配并启动(或确认已启动)的 workflow 数量;0 表示无人订阅。
|
|
113
247
|
*/
|
|
114
248
|
async function matchAndTrigger(
|
|
115
249
|
pg: PgClient,
|
|
116
250
|
temporalClient: Client,
|
|
251
|
+
outboxId: number,
|
|
117
252
|
eventName: string,
|
|
118
253
|
payload: Record<string, any>,
|
|
119
254
|
): Promise<number> {
|
|
@@ -150,40 +285,61 @@ async function matchAndTrigger(
|
|
|
150
285
|
if (!evaluateFilter(trigger.config.filterExpression, payload)) continue;
|
|
151
286
|
}
|
|
152
287
|
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
288
|
+
// 确定性 workflowId(用于幂等保护)
|
|
289
|
+
const deterministicWorkflowId = `wf-outbox-${outboxId}-w${row.workflow_id}`;
|
|
290
|
+
|
|
291
|
+
// 创建(或复用)WorkflowInstance —— 用 (workflow_id, source_outbox_id) UNIQUE 做幂等 upsert。
|
|
292
|
+
// 崩溃恢复路径下 (workflow_id, outboxId) 相同 → 直接拿到上次的 instanceId,不会产生孤儿行。
|
|
293
|
+
// 一个 outbox 事件可被多个 workflow 订阅,每个 workflow 各自得到一条 instance。
|
|
294
|
+
const instanceUpsert = await pg.query(
|
|
295
|
+
`INSERT INTO t_workflow_instance
|
|
296
|
+
(workflow_id, version_id, status, context, creator, source_outbox_id, created_at, updated_at)
|
|
297
|
+
VALUES ($1, $2, 'PENDING', $3, 'event_trigger', $4, NOW(), NOW())
|
|
298
|
+
ON CONFLICT (workflow_id, source_outbox_id) DO UPDATE
|
|
299
|
+
SET updated_at = NOW()
|
|
300
|
+
RETURNING id, (xmax = 0) AS inserted`,
|
|
301
|
+
[row.workflow_id, row.version_id, JSON.stringify(payload), outboxId],
|
|
159
302
|
);
|
|
160
|
-
const instanceId = Number(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
303
|
+
const instanceId = Number(instanceUpsert.rows[0].id);
|
|
304
|
+
const wasInserted = instanceUpsert.rows[0].inserted as boolean;
|
|
305
|
+
|
|
306
|
+
// 启动 Temporal workflow(幂等:同 workflowId 第二次启动会抛 AlreadyStartedError)
|
|
307
|
+
let temporalRunId = deterministicWorkflowId;
|
|
308
|
+
try {
|
|
309
|
+
const handle = await temporalClient.workflow.start("dslWorkflow", {
|
|
310
|
+
taskQueue: config.temporal.taskQueue,
|
|
311
|
+
workflowId: deterministicWorkflowId,
|
|
312
|
+
args: [
|
|
313
|
+
{
|
|
314
|
+
instanceId,
|
|
315
|
+
workflowId: Number(row.workflow_id),
|
|
316
|
+
versionId: Number(row.version_id),
|
|
317
|
+
dsl,
|
|
318
|
+
context: payload,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
});
|
|
322
|
+
temporalRunId = handle.workflowId;
|
|
323
|
+
console.log(
|
|
324
|
+
`[OutboxPoller] Triggered workflow ${row.workflow_id} for event "${eventName}", instance=${instanceId}${wasInserted ? "" : " (recovered)"}, wfId=${deterministicWorkflowId}`,
|
|
325
|
+
);
|
|
326
|
+
} catch (err: any) {
|
|
327
|
+
if (err instanceof WorkflowExecutionAlreadyStartedError) {
|
|
328
|
+
// 上一轮 start 成功但 outbox 提交前崩溃 —— 这次重放视为幂等成功
|
|
329
|
+
console.warn(
|
|
330
|
+
`[OutboxPoller] Workflow ${deterministicWorkflowId} already exists (recovery). outboxId=${outboxId}, instance=${instanceId}`,
|
|
331
|
+
);
|
|
332
|
+
} else {
|
|
333
|
+
throw err; // 其它错误冒泡,触发上层 ROLLBACK + retry
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// 回写 temporal_run_id(即便走的是 recovery 分支也确保字段不为空)
|
|
179
338
|
await pg.query(
|
|
180
339
|
`UPDATE t_workflow_instance SET temporal_run_id = $1 WHERE id = $2`,
|
|
181
|
-
[
|
|
340
|
+
[temporalRunId, instanceId],
|
|
182
341
|
);
|
|
183
342
|
|
|
184
|
-
console.log(
|
|
185
|
-
`[OutboxPoller] Triggered workflow ${row.workflow_id} for event "${eventName}", instance=${instanceId}`,
|
|
186
|
-
);
|
|
187
343
|
matchedCount++;
|
|
188
344
|
}
|
|
189
345
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as workflows from '../workflows/dsl-workflow';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smoke test: confirm cronTriggerWorkflow is exported from dsl-workflow.ts so the
|
|
6
|
+
* Temporal SDK auto-registers it via the workflowsPath setting in worker/src/index.ts.
|
|
7
|
+
*
|
|
8
|
+
* We don't run a TestWorkflowEnvironment here — that requires booting a real Temporal
|
|
9
|
+
* Server. The actual schedule registration + workflow execution paths are covered by
|
|
10
|
+
* the server-side TemporalService unit tests.
|
|
11
|
+
*/
|
|
12
|
+
describe('cronTriggerWorkflow export', () => {
|
|
13
|
+
it('is exported as a function', () => {
|
|
14
|
+
expect(typeof workflows.cronTriggerWorkflow).toBe('function');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('dslWorkflow is also still exported', () => {
|
|
18
|
+
expect(typeof workflows.dslWorkflow).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -41,10 +41,44 @@ import {
|
|
|
41
41
|
} from '../dsl/helpers';
|
|
42
42
|
|
|
43
43
|
// ─── Activity Proxies ─────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
//
|
|
45
|
+
// Activity 按"职责类别"分组配置 timeout / retry,避免一刀切。
|
|
46
|
+
// startToCloseTimeout 必须覆盖:业务上限(含 axios/vm 内部 timeout)+ Worker SIGTERM 后 cancel 响应窗口。
|
|
47
|
+
// 业务可在 dsl.config 里通过 `timeoutSec` 覆盖某次调用,这里的值是默认值/上限。
|
|
48
|
+
//
|
|
49
|
+
// 注意:单条 Activity 的最坏耗时 ≈ startToCloseTimeout × maximumAttempts。
|
|
50
|
+
// 若长于 K8s Pod 的 grace period(300s)会被 SIGKILL,把后续重试推给其它 worker。
|
|
51
|
+
|
|
52
|
+
// HTTP — axios 默认 timeout 30s,给 Activity 预留 cancel 响应余量
|
|
53
|
+
const httpActs = proxyActivities<
|
|
54
|
+
Pick<typeof import('../activities'), 'httpRequest'>
|
|
55
|
+
>({
|
|
46
56
|
startToCloseTimeout: '60s',
|
|
47
|
-
retry: { maximumAttempts: 3 },
|
|
57
|
+
retry: { maximumAttempts: 3, initialInterval: '1s', maximumInterval: '30s' },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// DB — 慢查询 / 大写入可能比较久;不重试写入(dbExecute 可能非幂等)
|
|
61
|
+
const dbActs = proxyActivities<
|
|
62
|
+
Pick<typeof import('../activities'), 'dbQuery' | 'dbExecute'>
|
|
63
|
+
>({
|
|
64
|
+
startToCloseTimeout: '120s',
|
|
65
|
+
retry: { maximumAttempts: 1 }, // 写入默认不重试,避免重复扣款类问题
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// 通知 —— 邮件/IM 网关一般快;失败重试要积极
|
|
69
|
+
const notifyActs = proxyActivities<
|
|
70
|
+
Pick<typeof import('../activities'), 'sendNotification'>
|
|
71
|
+
>({
|
|
72
|
+
startToCloseTimeout: '30s',
|
|
73
|
+
retry: { maximumAttempts: 5, initialInterval: '2s', maximumInterval: '60s' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 自定义 JS —— vm 内部默认 5s timeout,外层留足边界 + 不重试(脚本可能非幂等)
|
|
77
|
+
const codeActs = proxyActivities<
|
|
78
|
+
Pick<typeof import('../activities'), 'codeExecute'>
|
|
79
|
+
>({
|
|
80
|
+
startToCloseTimeout: '15s',
|
|
81
|
+
retry: { maximumAttempts: 1 },
|
|
48
82
|
});
|
|
49
83
|
|
|
50
84
|
const reportActs = proxyActivities<typeof import('../activities')>({
|
|
@@ -123,9 +157,22 @@ async function executeGraph(
|
|
|
123
157
|
const executed = new Set<string>();
|
|
124
158
|
const queue = [...startNodeIds];
|
|
125
159
|
|
|
160
|
+
// 防御:DSL 是循环图(A→B→A)或不可达 predecessor 时,
|
|
161
|
+
// 当前 BFS 会把 nodeId 不停 push 回 queue 形成死循环。
|
|
162
|
+
// 给一个上限:每个节点最多被处理 nodeCount * 2 次(包含一次重排队)。
|
|
163
|
+
const MAX_ITERATIONS = Math.max(dsl.nodes.length * 10, 100);
|
|
164
|
+
let iterations = 0;
|
|
165
|
+
|
|
126
166
|
while (queue.length > 0) {
|
|
127
167
|
if (isCancelled()) throw new CancellationScope().cancel;
|
|
128
168
|
|
|
169
|
+
if (++iterations > MAX_ITERATIONS) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`executeGraph exceeded ${MAX_ITERATIONS} iterations — DSL likely contains a cycle ` +
|
|
172
|
+
`or unreachable predecessors. Pending queue: [${queue.join(', ')}], executed: [${[...executed].join(', ')}]`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
129
176
|
const nodeId = queue.shift()!;
|
|
130
177
|
if (executed.has(nodeId)) continue;
|
|
131
178
|
|
|
@@ -217,27 +264,27 @@ async function executeNode(
|
|
|
217
264
|
|
|
218
265
|
// ─── Actions ────────────────────────────────────────────────────
|
|
219
266
|
case 'http_request': {
|
|
220
|
-
const result = await
|
|
267
|
+
const result = await httpActs.httpRequest(config as any);
|
|
221
268
|
return { output: result };
|
|
222
269
|
}
|
|
223
270
|
|
|
224
271
|
case 'db_query': {
|
|
225
|
-
const result = await
|
|
272
|
+
const result = await dbActs.dbQuery(config as any);
|
|
226
273
|
return { output: result };
|
|
227
274
|
}
|
|
228
275
|
|
|
229
276
|
case 'db_execute': {
|
|
230
|
-
const result = await
|
|
277
|
+
const result = await dbActs.dbExecute(config as any);
|
|
231
278
|
return { output: result };
|
|
232
279
|
}
|
|
233
280
|
|
|
234
281
|
case 'send_notification': {
|
|
235
|
-
const result = await
|
|
282
|
+
const result = await notifyActs.sendNotification(config as any);
|
|
236
283
|
return { output: result };
|
|
237
284
|
}
|
|
238
285
|
|
|
239
286
|
case 'code': {
|
|
240
|
-
const result = await
|
|
287
|
+
const result = await codeActs.codeExecute({
|
|
241
288
|
...config,
|
|
242
289
|
data: ctx,
|
|
243
290
|
} as any);
|
|
@@ -409,3 +456,44 @@ async function executeNode(
|
|
|
409
456
|
|
|
410
457
|
// ─── Helper Functions ─────────────────────────────────────────────────────────
|
|
411
458
|
// Pure functions are defined in ../dsl/helpers.ts and imported above.
|
|
459
|
+
|
|
460
|
+
// ─── Cron Trigger Workflow ────────────────────────────────────────────────────
|
|
461
|
+
//
|
|
462
|
+
// Started by Temporal Schedules (registered via NestJS TemporalService.upsertCronSchedule).
|
|
463
|
+
// Creates a t_workflow_instance row with creator='cron', then delegates to dslWorkflow
|
|
464
|
+
// as a child workflow so it shares all the per-node logic.
|
|
465
|
+
|
|
466
|
+
const cronActs = proxyActivities<typeof import('../activities')>({
|
|
467
|
+
startToCloseTimeout: '15s',
|
|
468
|
+
retry: { maximumAttempts: 5 },
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
export interface CronTriggerInput {
|
|
472
|
+
workflowId: number;
|
|
473
|
+
versionId: number;
|
|
474
|
+
dsl: WorkflowDSL;
|
|
475
|
+
scheduleId: string;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export async function cronTriggerWorkflow(
|
|
479
|
+
input: CronTriggerInput,
|
|
480
|
+
): Promise<WorkflowResult> {
|
|
481
|
+
const { instanceId } = await cronActs.createCronInstance({
|
|
482
|
+
workflowId: input.workflowId,
|
|
483
|
+
versionId: input.versionId,
|
|
484
|
+
scheduleId: input.scheduleId,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return await executeChild(dslWorkflow, {
|
|
488
|
+
workflowId: `wf-${input.workflowId}-${instanceId}`,
|
|
489
|
+
args: [
|
|
490
|
+
{
|
|
491
|
+
instanceId,
|
|
492
|
+
workflowId: input.workflowId,
|
|
493
|
+
versionId: input.versionId,
|
|
494
|
+
dsl: input.dsl,
|
|
495
|
+
context: { scheduledAt: new Date().toISOString(), scheduleId: input.scheduleId },
|
|
496
|
+
},
|
|
497
|
+
],
|
|
498
|
+
});
|
|
499
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# nginx.conf 核心配置
|
|
2
|
+
user nginx;
|
|
3
|
+
worker_processes auto;
|
|
4
|
+
|
|
5
|
+
events {
|
|
6
|
+
worker_connections 1024;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
http {
|
|
10
|
+
include /etc/nginx/mime.types;
|
|
11
|
+
default_type application/octet-stream;
|
|
12
|
+
server_tokens off;
|
|
13
|
+
|
|
14
|
+
# --------------------------
|
|
15
|
+
# 日志格式定义
|
|
16
|
+
# --------------------------
|
|
17
|
+
log_format custom '$remote_addr - $remote_user [$time_local] "$request" '
|
|
18
|
+
'$status $body_bytes_sent "$http_referer" ';
|
|
19
|
+
|
|
20
|
+
# --------------------------
|
|
21
|
+
# 安全头部配置
|
|
22
|
+
# --------------------------
|
|
23
|
+
add_header X-Content-Type-Options "nosniff";
|
|
24
|
+
add_header X-Frame-Options "SAMEORIGIN";
|
|
25
|
+
add_header X-XSS-Protection "1; mode=block";
|
|
26
|
+
#add_header Content-Security-Policy "default-src 'self'";
|
|
27
|
+
|
|
28
|
+
# --------------------------
|
|
29
|
+
# GZIP 压缩配置
|
|
30
|
+
# --------------------------
|
|
31
|
+
gzip on; # 启用 GZIP 压缩
|
|
32
|
+
gzip_comp_level 6; # 压缩级别(1-9,6 是性能与压缩率的平衡点)
|
|
33
|
+
gzip_min_length 1024; # 最小压缩文件大小(小于此值不压缩)
|
|
34
|
+
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; # 压缩的 MIME 类型
|
|
35
|
+
gzip_vary on; # 兼容不支持 GZIP 的客户端
|
|
36
|
+
gzip_proxied any; # 对所有代理请求启用压缩
|
|
37
|
+
|
|
38
|
+
# --------------------------
|
|
39
|
+
# SSL 配置(若启用 HTTPS)
|
|
40
|
+
# --------------------------
|
|
41
|
+
ssl_protocols TLSv1.2 TLSv1.3;
|
|
42
|
+
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
|
|
43
|
+
ssl_prefer_server_ciphers on;
|
|
44
|
+
|
|
45
|
+
server {
|
|
46
|
+
listen 80;
|
|
47
|
+
server_name _; # 接受所有域名(如果前面有反向代理,这里可以是默认值)
|
|
48
|
+
root /var/www/html;
|
|
49
|
+
|
|
50
|
+
autoindex off; # 关闭目录索引
|
|
51
|
+
index index.html;
|
|
52
|
+
|
|
53
|
+
location ~ \.(js|mjs)$ {
|
|
54
|
+
add_header Content-Type application/javascript;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# --------------------------
|
|
58
|
+
# SPA 路由处理 - 必须放在最后
|
|
59
|
+
# --------------------------
|
|
60
|
+
location / {
|
|
61
|
+
limit_except GET HEAD POST { # 仅允许 GET/HEAD/POST
|
|
62
|
+
deny all;
|
|
63
|
+
}
|
|
64
|
+
# 对于 SPA:先尝试文件,如果不存在直接回退到 index.html(不尝试目录,避免 403)
|
|
65
|
+
try_files $uri /index.html;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# --------------------------
|
|
69
|
+
# 日志格式(隐藏敏感信息)
|
|
70
|
+
# --------------------------
|
|
71
|
+
access_log /var/log/nginx/access.log custom;
|
|
72
|
+
error_log /var/log/nginx/error.log warn;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -653,6 +653,8 @@ export const ElementInspector: React.FC = () => {
|
|
|
653
653
|
const [freeInputMode, setFreeInputMode] = useState<
|
|
654
654
|
'freeInput' | 'requiresInput'
|
|
655
655
|
>('freeInput');
|
|
656
|
+
const [quickSelectVisible, setQuickSelectVisible] = useState(false);
|
|
657
|
+
const [quickInputVisible, setQuickInputVisible] = useState(false);
|
|
656
658
|
const pendingActionRef = useRef<InspectorAction | null>(null);
|
|
657
659
|
const pendingMenuInfoRef = useRef<InspectedElementInfo | null>(null);
|
|
658
660
|
|
|
@@ -1126,6 +1128,22 @@ export const ElementInspector: React.FC = () => {
|
|
|
1126
1128
|
onOk={handleFieldConfirmOk}
|
|
1127
1129
|
onCancel={handleFieldConfirmCancel}
|
|
1128
1130
|
/>
|
|
1131
|
+
|
|
1132
|
+
{/* Quick select modal */}
|
|
1133
|
+
<QuickSelectModal
|
|
1134
|
+
open={quickSelectVisible}
|
|
1135
|
+
action={pendingActionRef.current}
|
|
1136
|
+
onOk={handleQuickSelectOk}
|
|
1137
|
+
onCancel={handleQuickSelectCancel}
|
|
1138
|
+
/>
|
|
1139
|
+
|
|
1140
|
+
{/* Quick input modal */}
|
|
1141
|
+
<QuickInputModal
|
|
1142
|
+
open={quickInputVisible}
|
|
1143
|
+
action={pendingActionRef.current}
|
|
1144
|
+
onOk={handleQuickInputOk}
|
|
1145
|
+
onCancel={handleQuickInputCancel}
|
|
1146
|
+
/>
|
|
1129
1147
|
</>
|
|
1130
1148
|
);
|
|
1131
1149
|
};
|
|
@@ -29,7 +29,7 @@ export function generatePrompt(options: PromptOptions): string {
|
|
|
29
29
|
|
|
30
30
|
if (skill) {
|
|
31
31
|
parts.push(
|
|
32
|
-
`严格按照: ${skill} skill
|
|
32
|
+
`严格按照: ${skill} skill(位于 .claude/skills/${skill}/SKILL.md)描述的方法来完成代码的修改。如有不确定的地方,请先询问我,不要自己假设。`,
|
|
33
33
|
);
|
|
34
34
|
parts.push('');
|
|
35
35
|
}
|