@pactor-app/skills 1.24.0
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/LICENSE +21 -0
- package/README.md +40 -0
- package/package.json +26 -0
- package/pactor/SKILL.md +373 -0
- package/pactor/references/dsl-authoring.md +448 -0
- package/prd-to-app/SKILL.md +75 -0
- package/prd-to-app/references/acceptance-checklist.md +47 -0
- package/prd-to-app/references/component-mapping.md +41 -0
- package/prd-to-app/references/mock-data-routes.md +66 -0
- package/prd-to-app/references/page-templates.md +274 -0
- package/prd-to-app/references/project-structure.md +89 -0
- package/prd-to-app/references/theme-and-style.md +41 -0
- package/prd-to-app/template/README.md +26 -0
- package/prd-to-app/template/app/[[...slug]]/page.tsx +7 -0
- package/prd-to-app/template/app/api/auth/login/route.ts +32 -0
- package/prd-to-app/template/app/api/auth/logout/route.ts +22 -0
- package/prd-to-app/template/app/api/data/[...parts]/route.ts +118 -0
- package/prd-to-app/template/app/api/public/pages/[id]/route.ts +40 -0
- package/prd-to-app/template/app/api/sitemap/route.ts +21 -0
- package/prd-to-app/template/app/api/users/route.ts +57 -0
- package/prd-to-app/template/app/favicon.ico +0 -0
- package/prd-to-app/template/app/globals.css +248 -0
- package/prd-to-app/template/app/layout.tsx +49 -0
- package/prd-to-app/template/app/login.css +122 -0
- package/prd-to-app/template/eslint.config.mjs +18 -0
- package/prd-to-app/template/next-env.d.ts +7 -0
- package/prd-to-app/template/next.config.js +32 -0
- package/prd-to-app/template/package.json +34 -0
- package/prd-to-app/template/pnpm-lock.yaml +6854 -0
- package/prd-to-app/template/pnpm-workspace.yaml +7 -0
- package/prd-to-app/template/postcss.config.mjs +7 -0
- package/prd-to-app/template/public/file.svg +1 -0
- package/prd-to-app/template/public/globe.svg +1 -0
- package/prd-to-app/template/public/next.svg +1 -0
- package/prd-to-app/template/public/skins/chenwunaifu.css +111 -0
- package/prd-to-app/template/public/skins/taijingshenlin.css +109 -0
- package/prd-to-app/template/public/skins/xuanzhimoyun.css +110 -0
- package/prd-to-app/template/public/vercel.svg +1 -0
- package/prd-to-app/template/public/window.svg +1 -0
- package/prd-to-app/template/scripts/build.mjs +35 -0
- package/prd-to-app/template/src/pactor/App.tsx +55 -0
- package/prd-to-app/template/src/pactor/actions.ts +438 -0
- package/prd-to-app/template/src/pactor/components/card-links.tsx +86 -0
- package/prd-to-app/template/src/pactor/components/chip-filter.tsx +89 -0
- package/prd-to-app/template/src/pactor/components/chip-node.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/debug-chat.tsx +189 -0
- package/prd-to-app/template/src/pactor/components/index.ts +28 -0
- package/prd-to-app/template/src/pactor/components/log-status.tsx +64 -0
- package/prd-to-app/template/src/pactor/components/status-badge.tsx +46 -0
- package/prd-to-app/template/src/pactor/components/theme-switcher.tsx +136 -0
- package/prd-to-app/template/src/pactor/config.ts +28 -0
- package/prd-to-app/template/src/pactor/mocks/audit-mock.ts +403 -0
- package/prd-to-app/template/src/pactor/mocks/call-logs-mock.ts +875 -0
- package/prd-to-app/template/src/pactor/mocks/client-debug-mock.ts +188 -0
- package/prd-to-app/template/src/pactor/mocks/client-templates-mock.ts +497 -0
- package/prd-to-app/template/src/pactor/mocks/clients-mock.ts +483 -0
- package/prd-to-app/template/src/pactor/mocks/costs-mock.ts +406 -0
- package/prd-to-app/template/src/pactor/mocks/dashboard-mock.ts +192 -0
- package/prd-to-app/template/src/pactor/mocks/model-debug-mock.ts +144 -0
- package/prd-to-app/template/src/pactor/mocks/models-mock.ts +799 -0
- package/prd-to-app/template/src/pactor/mocks/ops-usage-mock.ts +806 -0
- package/prd-to-app/template/src/pactor/mocks/providers-mock.ts +424 -0
- package/prd-to-app/template/src/pactor/mocks/routes-mock.ts +604 -0
- package/prd-to-app/template/src/pactor/services.ts +185 -0
- package/prd-to-app/template/src/pactor/skin.ts +52 -0
- package/prd-to-app/template/src/server/accounts.ts +63 -0
- package/prd-to-app/template/src/server/cors.ts +34 -0
- package/prd-to-app/template/src/server/sitemap.ts +176 -0
- package/prd-to-app/template/src/server/yaml/common/dashboard.yaml +498 -0
- package/prd-to-app/template/src/server/yaml/common/login.yaml +84 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-create.yaml +340 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-detail.yaml +780 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-edit.yaml +366 -0
- package/prd-to-app/template/src/server/yaml/provider/provider-list.yaml +1989 -0
- package/prd-to-app/template/src/server/yaml/route/route-create.yaml +511 -0
- package/prd-to-app/template/src/server/yaml/route/route-detail.yaml +610 -0
- package/prd-to-app/template/src/server/yaml/route/route-edit.yaml +809 -0
- package/prd-to-app/template/src/server/yaml/route/route-list.yaml +1793 -0
- package/prd-to-app/template/src/server/yaml/sitemap.yaml +250 -0
- package/prd-to-app/template/tsconfig.json +34 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 页面级自定义动作(调试台等场景的示例实现
|
|
3
|
+
* model-debug-actions.ts,合并为单文件),经 SitemapApp 的 `actions` prop
|
|
4
|
+
* 全局注册。
|
|
5
|
+
* DSL 表达式不支持数组操作 / 定时器 / 剪贴板,故对话状态变更、流式模拟、
|
|
6
|
+
* 身份联动选项计算收敛在此处;handler 经 ctx.context.state 直接读写页面
|
|
7
|
+
* StateStore,写入即触发 DSL 树重渲染(与内置 setState 动作同一通道)。
|
|
8
|
+
*
|
|
9
|
+
* ActionHandler 类型见 @pactor-app/runtime;此处按结构化子集声明参数类型
|
|
10
|
+
* (handler 只需已求值的 action.params 与 state 读写),赋值给 SitemapApp
|
|
11
|
+
* actions 选项时由 TS 结构化检查兼容性(1.22.0 签名:
|
|
12
|
+
* (action: ActionConfig, ctx: ActionContext) => ActionResult | Promise<…>)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ActionHandler } from "@pactor-app/runtime";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
appKeyOptionsOf,
|
|
19
|
+
routeOptionsOf,
|
|
20
|
+
routeTagOptionsOf,
|
|
21
|
+
simulateClientCall,
|
|
22
|
+
type ClientDebugRound,
|
|
23
|
+
} from "./mocks/client-debug-mock";
|
|
24
|
+
import { getProvider } from "./mocks/providers-mock";
|
|
25
|
+
import {
|
|
26
|
+
capabilitiesOf,
|
|
27
|
+
modelOptionsOf,
|
|
28
|
+
paramDefaultsOf,
|
|
29
|
+
simulateModelCall,
|
|
30
|
+
supportsReasoning,
|
|
31
|
+
testConnection,
|
|
32
|
+
} from "./mocks/model-debug-mock";
|
|
33
|
+
import { streamText, type DebugChatMessage } from "./components/debug-chat";
|
|
34
|
+
|
|
35
|
+
/** SitemapApp actions 选项 handler 的结构化最小契约(ActionHandler 子集) */
|
|
36
|
+
export interface DebugActionContext {
|
|
37
|
+
context: {
|
|
38
|
+
state: {
|
|
39
|
+
get: (path: string) => unknown;
|
|
40
|
+
set: (patch: Record<string, unknown>) => void;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type DebugAction = (
|
|
46
|
+
action: { params?: Record<string, unknown> },
|
|
47
|
+
ctx: DebugActionContext,
|
|
48
|
+
) => { ok: boolean };
|
|
49
|
+
|
|
50
|
+
function str(value: unknown): string {
|
|
51
|
+
return typeof value === "string" ? value : "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function num(value: unknown): number | undefined {
|
|
55
|
+
return typeof value === "number" ? value : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createClientDebugActions(): Record<string, DebugAction> {
|
|
59
|
+
/** 流式定时器的停止闭包(停止生成 / 清空对话 / 下一次发送前取消) */
|
|
60
|
+
let stopCurrent: (() => void) | null = null;
|
|
61
|
+
|
|
62
|
+
/** 流式收尾:assistant 消息落定 + 轮次写入 selectedRound(右侧返回报文此时可见) */
|
|
63
|
+
const finalize = (
|
|
64
|
+
ctx: DebugActionContext,
|
|
65
|
+
round: ClientDebugRound,
|
|
66
|
+
assistantId: string,
|
|
67
|
+
finalText: string,
|
|
68
|
+
stopped: boolean,
|
|
69
|
+
) => {
|
|
70
|
+
const messages = (
|
|
71
|
+
(ctx.context.state.get("messages") as DebugChatMessage[]) ?? []
|
|
72
|
+
).map((m) =>
|
|
73
|
+
m.id === assistantId
|
|
74
|
+
? {
|
|
75
|
+
...m,
|
|
76
|
+
content: finalText,
|
|
77
|
+
streaming: false,
|
|
78
|
+
meta: `Token ${round.tokensIn}/${round.tokensOut} · TTFT ${round.ttftMs}ms · 延迟 ${round.latencyMs}ms${stopped ? " · 已停止" : ""}`,
|
|
79
|
+
}
|
|
80
|
+
: m,
|
|
81
|
+
);
|
|
82
|
+
ctx.context.state.set({
|
|
83
|
+
messages,
|
|
84
|
+
selectedRound: round,
|
|
85
|
+
selectedRoundId: round.id,
|
|
86
|
+
streaming: false,
|
|
87
|
+
});
|
|
88
|
+
stopCurrent = null;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
/** 切换客户端:联动 AppKey(取该客户端唯一一把)、授权路由与上下文 */
|
|
93
|
+
"clientDebug.selectClient": (action, ctx) => {
|
|
94
|
+
const clientCode = str(action.params?.clientCode);
|
|
95
|
+
const appKeys = appKeyOptionsOf(clientCode);
|
|
96
|
+
ctx.context.state.set({
|
|
97
|
+
clientCode,
|
|
98
|
+
appKeyOptions: appKeys,
|
|
99
|
+
appKeyId: appKeys[0]?.value ?? "",
|
|
100
|
+
routeOptions: routeOptionsOf(clientCode),
|
|
101
|
+
routeCode: "",
|
|
102
|
+
tagOptions: [],
|
|
103
|
+
routeTag: "",
|
|
104
|
+
});
|
|
105
|
+
return { ok: true };
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/** 切换授权路由:联动 RouteTag 选项 */
|
|
109
|
+
"clientDebug.selectRoute": (action, ctx) => {
|
|
110
|
+
const routeCode = str(action.params?.routeCode);
|
|
111
|
+
ctx.context.state.set({
|
|
112
|
+
routeCode,
|
|
113
|
+
tagOptions: routeTagOptionsOf(routeCode),
|
|
114
|
+
routeTag: "",
|
|
115
|
+
});
|
|
116
|
+
return { ok: true };
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/** 发送:模拟完整链路调用,流式渲染 assistant 回复 */
|
|
120
|
+
"clientDebug.send": (action, ctx) => {
|
|
121
|
+
const state = ctx.context.state;
|
|
122
|
+
if (state.get("streaming")) return { ok: false };
|
|
123
|
+
const clientCode = str(state.get("clientCode"));
|
|
124
|
+
const appKeyId = str(state.get("appKeyId"));
|
|
125
|
+
const routeCode = str(state.get("routeCode"));
|
|
126
|
+
const text = str(action.params?.text);
|
|
127
|
+
if (!clientCode || !appKeyId || !routeCode || !text.trim()) {
|
|
128
|
+
return { ok: false };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const roundCount = Number(state.get("roundCount") ?? 0);
|
|
132
|
+
const round = simulateClientCall(
|
|
133
|
+
{
|
|
134
|
+
clientCode,
|
|
135
|
+
appKeyId,
|
|
136
|
+
routeCode,
|
|
137
|
+
channel: str(state.get("channel")),
|
|
138
|
+
routeTag: str(state.get("routeTag")),
|
|
139
|
+
user: str(state.get("user")),
|
|
140
|
+
},
|
|
141
|
+
text,
|
|
142
|
+
roundCount + 1,
|
|
143
|
+
);
|
|
144
|
+
const assistantId = `${round.id}-a`;
|
|
145
|
+
const messages: DebugChatMessage[] = [
|
|
146
|
+
...((state.get("messages") as DebugChatMessage[]) ?? []),
|
|
147
|
+
{ id: `${round.id}-u`, role: "user", content: text, round },
|
|
148
|
+
{
|
|
149
|
+
id: assistantId,
|
|
150
|
+
role: "assistant",
|
|
151
|
+
content: "",
|
|
152
|
+
round,
|
|
153
|
+
streaming: true,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
state.set({
|
|
157
|
+
messages,
|
|
158
|
+
streaming: true,
|
|
159
|
+
roundCount: roundCount + 1,
|
|
160
|
+
// 流结束后再写入 selectedRound(PRD:返回报文在流结束后刷新)
|
|
161
|
+
selectedRound: null,
|
|
162
|
+
selectedRoundId: round.id,
|
|
163
|
+
tab: "request",
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
let latest = "";
|
|
167
|
+
const cancel = streamText(
|
|
168
|
+
round.reply,
|
|
169
|
+
(chunk) => {
|
|
170
|
+
latest = chunk;
|
|
171
|
+
const cur =
|
|
172
|
+
(ctx.context.state.get("messages") as DebugChatMessage[]) ?? [];
|
|
173
|
+
ctx.context.state.set({
|
|
174
|
+
messages: cur.map((m) =>
|
|
175
|
+
m.id === assistantId ? { ...m, content: chunk } : m,
|
|
176
|
+
),
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
() => finalize(ctx, round, assistantId, round.reply, false),
|
|
180
|
+
);
|
|
181
|
+
stopCurrent = () => {
|
|
182
|
+
cancel();
|
|
183
|
+
finalize(ctx, round, assistantId, latest || round.reply.slice(0, 12), true);
|
|
184
|
+
};
|
|
185
|
+
return { ok: true };
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/** 停止生成:取消定时器并按当前片段收尾 */
|
|
189
|
+
"clientDebug.stop": (_action, ctx) => {
|
|
190
|
+
stopCurrent?.();
|
|
191
|
+
if (ctx.context.state.get("streaming")) {
|
|
192
|
+
ctx.context.state.set({ streaming: false });
|
|
193
|
+
}
|
|
194
|
+
return { ok: true };
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
/** 清空对话:清空本地会话(不影响已落库调用日志) */
|
|
198
|
+
"clientDebug.clear": (_action, ctx) => {
|
|
199
|
+
stopCurrent?.();
|
|
200
|
+
stopCurrent = null;
|
|
201
|
+
ctx.context.state.set({
|
|
202
|
+
messages: [],
|
|
203
|
+
selectedRound: null,
|
|
204
|
+
selectedRoundId: "",
|
|
205
|
+
roundCount: 0,
|
|
206
|
+
streaming: false,
|
|
207
|
+
});
|
|
208
|
+
return { ok: true };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/** 复制报文到剪贴板(DSL 无剪贴板能力,经动作桥接) */
|
|
212
|
+
"clientDebug.copy": (action) => {
|
|
213
|
+
const text = action.params?.text;
|
|
214
|
+
if (typeof text === "string" && text) {
|
|
215
|
+
void navigator.clipboard?.writeText(text);
|
|
216
|
+
}
|
|
217
|
+
return { ok: true };
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function createModelDebugActions(): Record<string, DebugAction> {
|
|
223
|
+
let stopCurrent: (() => void) | null = null;
|
|
224
|
+
let seq = 0;
|
|
225
|
+
|
|
226
|
+
/** assistant 消息收尾(流式完成 / 停止生成共用) */
|
|
227
|
+
const finalize = (
|
|
228
|
+
ctx: DebugActionContext,
|
|
229
|
+
id: string,
|
|
230
|
+
patch: Partial<DebugChatMessage>,
|
|
231
|
+
) => {
|
|
232
|
+
const messages = (
|
|
233
|
+
(ctx.context.state.get("messages") as DebugChatMessage[]) ?? []
|
|
234
|
+
).map((m) => (m.id === id ? { ...m, ...patch } : m));
|
|
235
|
+
ctx.context.state.set({ messages, streaming: false });
|
|
236
|
+
stopCurrent = null;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
/** 切换供应商:联动刷新模型列表并清空已选模型与连接状态 */
|
|
241
|
+
"modelDebug.selectProvider": (action, ctx) => {
|
|
242
|
+
const providerCode = str(action.params?.providerCode);
|
|
243
|
+
ctx.context.state.set({
|
|
244
|
+
providerCode,
|
|
245
|
+
modelOptions: modelOptionsOf(providerCode),
|
|
246
|
+
modelCode: "",
|
|
247
|
+
conn: null,
|
|
248
|
+
showReasoning: false,
|
|
249
|
+
canStream: true,
|
|
250
|
+
canTools: false,
|
|
251
|
+
temperature: undefined,
|
|
252
|
+
topP: undefined,
|
|
253
|
+
maxTokens: undefined,
|
|
254
|
+
streamingOn: true,
|
|
255
|
+
toolsOn: false,
|
|
256
|
+
});
|
|
257
|
+
return { ok: true };
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/** 切换模型:回填默认参数 + 按 Capability 计算参数显隐/开关可用性 */
|
|
261
|
+
"modelDebug.selectModel": (action, ctx) => {
|
|
262
|
+
const modelCode = str(action.params?.modelCode);
|
|
263
|
+
const caps = capabilitiesOf(modelCode);
|
|
264
|
+
const defaults = paramDefaultsOf(modelCode);
|
|
265
|
+
ctx.context.state.set({
|
|
266
|
+
modelCode,
|
|
267
|
+
conn: null,
|
|
268
|
+
temperature: defaults.temperature,
|
|
269
|
+
topP: defaults.topP,
|
|
270
|
+
maxTokens: defaults.maxTokens,
|
|
271
|
+
reasoningEffort: defaults.reasoningEffort ?? "medium",
|
|
272
|
+
showReasoning: supportsReasoning(modelCode),
|
|
273
|
+
canStream: caps.length === 0 || caps.includes("Streaming"),
|
|
274
|
+
canTools: caps.includes("Tool Calling"),
|
|
275
|
+
streamingOn: caps.includes("Streaming"),
|
|
276
|
+
toolsOn: false,
|
|
277
|
+
});
|
|
278
|
+
return { ok: true };
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
/** 连接测试:不发对话,仅探测凭证与 Endpoint 可达性(模拟 600ms 探测延迟) */
|
|
282
|
+
"modelDebug.testConnection": (_action, ctx) => {
|
|
283
|
+
const state = ctx.context.state;
|
|
284
|
+
const providerCode = str(state.get("providerCode"));
|
|
285
|
+
if (!providerCode || !str(state.get("modelCode")) || state.get("testing")) {
|
|
286
|
+
return { ok: false };
|
|
287
|
+
}
|
|
288
|
+
state.set({ testing: true, conn: null });
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
ctx.context.state.set({
|
|
291
|
+
conn: testConnection(providerCode),
|
|
292
|
+
testing: false,
|
|
293
|
+
});
|
|
294
|
+
}, 600);
|
|
295
|
+
return { ok: true };
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
/** 发送:直连供应商模型(不经过 AppKey / Route 业务链路) */
|
|
299
|
+
"modelDebug.send": (action, ctx) => {
|
|
300
|
+
const state = ctx.context.state;
|
|
301
|
+
if (state.get("streaming")) return { ok: false };
|
|
302
|
+
const providerCode = str(state.get("providerCode"));
|
|
303
|
+
const modelCode = str(state.get("modelCode"));
|
|
304
|
+
const text = str(action.params?.text);
|
|
305
|
+
if (!providerCode || !modelCode || !text.trim()) return { ok: false };
|
|
306
|
+
|
|
307
|
+
seq += 1;
|
|
308
|
+
const msgId = `m-${seq}`;
|
|
309
|
+
const messages = (state.get("messages") as DebugChatMessage[]) ?? [];
|
|
310
|
+
const userMsg: DebugChatMessage = {
|
|
311
|
+
id: `${msgId}-u`,
|
|
312
|
+
role: "user",
|
|
313
|
+
content: text,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// 上游/网关失败(凭证未配置):对话区以错误消息展示摘要(PRD 交互规则 3)
|
|
317
|
+
const provider = getProvider(providerCode);
|
|
318
|
+
if (provider && provider.keyStatus !== "configured") {
|
|
319
|
+
state.set({
|
|
320
|
+
messages: [
|
|
321
|
+
...messages,
|
|
322
|
+
userMsg,
|
|
323
|
+
{
|
|
324
|
+
id: `${msgId}-e`,
|
|
325
|
+
role: "error",
|
|
326
|
+
content: `请求失败:Provider「${provider.displayName}」${
|
|
327
|
+
provider.keyStatus === "pending"
|
|
328
|
+
? "API Key 待配置"
|
|
329
|
+
: "未配置 API Key"
|
|
330
|
+
},请先在 Provider 页完成凭证配置。`,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
return { ok: true };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const streamingOn =
|
|
338
|
+
state.get("streamingOn") === true && state.get("canStream") !== false;
|
|
339
|
+
const result = simulateModelCall(
|
|
340
|
+
providerCode,
|
|
341
|
+
modelCode,
|
|
342
|
+
str(state.get("systemPrompt")),
|
|
343
|
+
{
|
|
344
|
+
temperature: num(state.get("temperature")),
|
|
345
|
+
topP: num(state.get("topP")),
|
|
346
|
+
maxTokens: num(state.get("maxTokens")),
|
|
347
|
+
reasoningEffort:
|
|
348
|
+
state.get("showReasoning") === true
|
|
349
|
+
? str(state.get("reasoningEffort"))
|
|
350
|
+
: undefined,
|
|
351
|
+
streaming: streamingOn,
|
|
352
|
+
tools:
|
|
353
|
+
state.get("toolsOn") === true && state.get("canTools") === true,
|
|
354
|
+
},
|
|
355
|
+
text,
|
|
356
|
+
);
|
|
357
|
+
const meta = `Token ${result.tokensIn}/${result.tokensOut} · TTFT ${result.ttftMs}ms · 延迟 ${result.latencyMs}ms · 上游 ${result.status}`;
|
|
358
|
+
|
|
359
|
+
// 非流式:直接落定完整回复
|
|
360
|
+
if (!streamingOn) {
|
|
361
|
+
state.set({
|
|
362
|
+
messages: [
|
|
363
|
+
...messages,
|
|
364
|
+
userMsg,
|
|
365
|
+
{
|
|
366
|
+
id: msgId,
|
|
367
|
+
role: "assistant",
|
|
368
|
+
content: result.reply,
|
|
369
|
+
meta,
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
});
|
|
373
|
+
return { ok: true };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 流式:边收边渲染;可停止生成
|
|
377
|
+
state.set({
|
|
378
|
+
messages: [
|
|
379
|
+
...messages,
|
|
380
|
+
userMsg,
|
|
381
|
+
{ id: msgId, role: "assistant", content: "", streaming: true },
|
|
382
|
+
],
|
|
383
|
+
streaming: true,
|
|
384
|
+
});
|
|
385
|
+
let latest = "";
|
|
386
|
+
const cancel = streamText(
|
|
387
|
+
result.reply,
|
|
388
|
+
(chunk) => {
|
|
389
|
+
latest = chunk;
|
|
390
|
+
const cur =
|
|
391
|
+
(ctx.context.state.get("messages") as DebugChatMessage[]) ?? [];
|
|
392
|
+
ctx.context.state.set({
|
|
393
|
+
messages: cur.map((m) =>
|
|
394
|
+
m.id === msgId ? { ...m, content: chunk } : m,
|
|
395
|
+
),
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
() => finalize(ctx, msgId, { streaming: false, meta }),
|
|
399
|
+
);
|
|
400
|
+
stopCurrent = () => {
|
|
401
|
+
cancel();
|
|
402
|
+
finalize(ctx, msgId, {
|
|
403
|
+
content: latest || result.reply.slice(0, 12),
|
|
404
|
+
streaming: false,
|
|
405
|
+
meta: `${meta} · 已停止`,
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
return { ok: true };
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/** 停止生成 */
|
|
412
|
+
"modelDebug.stop": (_action, ctx) => {
|
|
413
|
+
stopCurrent?.();
|
|
414
|
+
if (ctx.context.state.get("streaming")) {
|
|
415
|
+
ctx.context.state.set({ streaming: false });
|
|
416
|
+
}
|
|
417
|
+
return { ok: true };
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
/** 清空对话(不影响服务端) */
|
|
421
|
+
"modelDebug.clear": (_action, ctx) => {
|
|
422
|
+
stopCurrent?.();
|
|
423
|
+
stopCurrent = null;
|
|
424
|
+
ctx.context.state.set({ messages: [], streaming: false });
|
|
425
|
+
return { ok: true };
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* SitemapApp 全局动作注册表(clientDebug.* / modelDebug.*)。
|
|
432
|
+
* 单例:两组动作名不相交;同一时刻 PageOutlet 只挂载一个页面,
|
|
433
|
+
* 各页面的流式停止闭包互不干扰。
|
|
434
|
+
*/
|
|
435
|
+
export const pactorActions: Record<string, ActionHandler> = {
|
|
436
|
+
...createClientDebugActions(),
|
|
437
|
+
...createModelDebugActions(),
|
|
438
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { CirclePlay, Ban } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 卡片链接与行内操作组件(列表页卡片常用内联定义,
|
|
9
|
+
* ProviderCardLink / ModelCardLink / RouteCardLink / TemplateCardLink 四份
|
|
10
|
+
* 实现完全相同,此处合并为一个 CardLink 并以别名注册)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** DSL `type: "ProviderCardLink" | "ModelCardLink" | "RouteCardLink" | "TemplateCardLink"`:
|
|
14
|
+
* 整卡点击跳详情(hover 手型/阴影见 globals.css .provider-card / .model-card);
|
|
15
|
+
* 内部按钮/开关/链接的点击经 closest 检测屏蔽,不触发跳转 */
|
|
16
|
+
export function CardLink({
|
|
17
|
+
href,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
href?: string;
|
|
21
|
+
children?: ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className="h-full"
|
|
27
|
+
onClick={(e) => {
|
|
28
|
+
if (
|
|
29
|
+
(e.target as HTMLElement).closest("button, a, input, [role='switch']")
|
|
30
|
+
)
|
|
31
|
+
return;
|
|
32
|
+
if (href) router.push(href);
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** DSL `type: "StatusToggleIcon"`:启停状态图标(DSL Icon 白名单无 CirclePlay/Ban,
|
|
41
|
+
* 故直接引 lucide-react):启用 → CirclePlay(主色);停用 → Ban(灰显) */
|
|
42
|
+
export function StatusToggleIcon({ enabled }: { enabled?: boolean }) {
|
|
43
|
+
const ToggleIcon = !enabled ? CirclePlay : Ban;
|
|
44
|
+
return (
|
|
45
|
+
<ToggleIcon
|
|
46
|
+
size={16}
|
|
47
|
+
color={enabled ? "var(--primary)" : "var(--muted-foreground)"}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** DSL `type: "TextLink"`:文本形态可点区域(Text 组件不挂事件,Renderer 会把
|
|
53
|
+
* events.onClick 注入 props,这里以 span 承接;stopPropagation 避免触发整卡跳转) */
|
|
54
|
+
export function TextLink({
|
|
55
|
+
text,
|
|
56
|
+
className,
|
|
57
|
+
style,
|
|
58
|
+
onClick,
|
|
59
|
+
}: {
|
|
60
|
+
text?: string;
|
|
61
|
+
className?: string;
|
|
62
|
+
style?: CSSProperties;
|
|
63
|
+
onClick?: () => void;
|
|
64
|
+
}) {
|
|
65
|
+
return (
|
|
66
|
+
<span
|
|
67
|
+
role="button"
|
|
68
|
+
tabIndex={0}
|
|
69
|
+
className={className}
|
|
70
|
+
style={{ cursor: "pointer", ...style }}
|
|
71
|
+
onClick={(e) => {
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
onClick?.();
|
|
74
|
+
}}
|
|
75
|
+
onKeyDown={(e) => {
|
|
76
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
e.stopPropagation();
|
|
79
|
+
onClick?.();
|
|
80
|
+
}
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{text}
|
|
84
|
+
</span>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 快筛 Chip 组(列表页共享的自定义组件):Chip 列表整体作为单一组件,
|
|
5
|
+
* 选项(value / 文案 / 计数 / 色点)由接口返回,页面 DSL 不再逐枚硬编码 ChipNode。
|
|
6
|
+
* 选项文案含计数,由接口侧预计算(表达式引擎不支持数组方法与函数调用)。
|
|
7
|
+
*
|
|
8
|
+
* 兼容各列表页的两种交互(Provider / Model / Route / Client / ClientTemplate):
|
|
9
|
+
* - mode="single":单选,value 为字符串。点击已选项回到 allValue(默认 'all'),
|
|
10
|
+
* 与「状态 / 类型」单选快筛的 toggle 语义一致;
|
|
11
|
+
* - mode="multi":多选,value 为已选值数组。选项中含 value === allValue 的
|
|
12
|
+
* 「全部」项时,点击它清空选择、仅在无选择时高亮(标签 / 厂商快筛语义);
|
|
13
|
+
* 不含「全部」项时纯切换、不选即全部(Provider 类型快筛语义)。
|
|
14
|
+
*
|
|
15
|
+
* DSL 用法(${event} = 新值,single 为字符串 / multi 为数组):
|
|
16
|
+
* {
|
|
17
|
+
* type: "ChipFilter",
|
|
18
|
+
* props: {
|
|
19
|
+
* mode: single,
|
|
20
|
+
* value: "${state.statusChip}",
|
|
21
|
+
* items: "${data.providers.filters.status}",
|
|
22
|
+
* },
|
|
23
|
+
* events: { onChange: [setState, reload] },
|
|
24
|
+
* }
|
|
25
|
+
*/
|
|
26
|
+
import { ChipNode } from "./chip-node";
|
|
27
|
+
|
|
28
|
+
export interface ChipFilterItem {
|
|
29
|
+
/** 提交值('all' 为约定的「全部」项,可用 allValue 改写) */
|
|
30
|
+
value: string;
|
|
31
|
+
/** 展示文案(含计数,接口预计算) */
|
|
32
|
+
text: string;
|
|
33
|
+
/** 可选状态色点 */
|
|
34
|
+
dot?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function ChipFilter({
|
|
38
|
+
items,
|
|
39
|
+
mode = "single",
|
|
40
|
+
value,
|
|
41
|
+
allValue = "all",
|
|
42
|
+
onChange,
|
|
43
|
+
}: {
|
|
44
|
+
items?: ChipFilterItem[];
|
|
45
|
+
mode?: "single" | "multi";
|
|
46
|
+
value?: string | string[];
|
|
47
|
+
allValue?: string;
|
|
48
|
+
onChange?: (value: string | string[]) => void;
|
|
49
|
+
}) {
|
|
50
|
+
if (!Array.isArray(items)) return null;
|
|
51
|
+
const selectedValues = Array.isArray(value) ? value : [];
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{items.map((item) => {
|
|
55
|
+
const selected =
|
|
56
|
+
mode === "multi"
|
|
57
|
+
? item.value === allValue
|
|
58
|
+
? selectedValues.length === 0
|
|
59
|
+
: selectedValues.includes(item.value)
|
|
60
|
+
: (value ?? allValue) === item.value;
|
|
61
|
+
return (
|
|
62
|
+
<ChipNode
|
|
63
|
+
key={item.value}
|
|
64
|
+
text={item.text}
|
|
65
|
+
dot={item.dot}
|
|
66
|
+
selected={selected}
|
|
67
|
+
onClick={() => {
|
|
68
|
+
if (mode === "multi") {
|
|
69
|
+
if (item.value === allValue) {
|
|
70
|
+
onChange?.([]);
|
|
71
|
+
} else {
|
|
72
|
+
onChange?.(
|
|
73
|
+
selected
|
|
74
|
+
? selectedValues.filter((v) => v !== item.value)
|
|
75
|
+
: [...selectedValues, item.value],
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
onChange?.(
|
|
80
|
+
selected && item.value !== allValue ? allValue : item.value,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 快筛 Chip(Provider / Model / Route 列表页共享的自定义组件)。
|
|
5
|
+
* 未选中 = outline 小按钮(border-border,hover 走 accent);
|
|
6
|
+
* 选中 = 主色实底 + 主色前景文字(一眼可辨的激活态)。
|
|
7
|
+
* text / selected 由运行时对 props 模板求值后传入(selected 接收布尔表达式结果);
|
|
8
|
+
* dot 为可选状态色点(Provider 状态快筛使用);
|
|
9
|
+
* 点击动作由 DSL 节点 events.onClick 注入(运行时执行为回调,语义同 Button onClick)。
|
|
10
|
+
*
|
|
11
|
+
* DSL 用法:
|
|
12
|
+
* {
|
|
13
|
+
* type: "ChipNode",
|
|
14
|
+
* props: {
|
|
15
|
+
* text: "全部 ${counts.all.enabled}/${counts.all.total}",
|
|
16
|
+
* selected: "${state.statusChip == 'all'}",
|
|
17
|
+
* },
|
|
18
|
+
* events: { onClick: [setState / reload ...] },
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
export function ChipNode({
|
|
22
|
+
text,
|
|
23
|
+
selected,
|
|
24
|
+
dot,
|
|
25
|
+
onClick,
|
|
26
|
+
}: {
|
|
27
|
+
text?: string;
|
|
28
|
+
selected?: boolean;
|
|
29
|
+
dot?: string;
|
|
30
|
+
onClick?: () => void;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
onClick={onClick}
|
|
36
|
+
className={`inline-flex h-8 shrink-0 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-xl border px-3 text-xs font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 ${
|
|
37
|
+
selected
|
|
38
|
+
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
|
39
|
+
: "border-border bg-card hover:bg-accent hover:text-accent-foreground"
|
|
40
|
+
}`}
|
|
41
|
+
>
|
|
42
|
+
{dot ? <span style={{ color: dot, fontSize: 10 }}>●</span> : null}
|
|
43
|
+
{text}
|
|
44
|
+
</button>
|
|
45
|
+
);
|
|
46
|
+
}
|