product-spec-mcp 0.4.0 → 0.4.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.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/dist/index.cjs +1 -1
- package/docs/connect-flow.md +26 -1
- package/docs/online-pm-gate.md +2 -0
- package/package.json +2 -1
- package/workers/migrations/0002_connect_metadata.sql +2 -0
- package/workers/pm-intent-gate.mjs +186 -7
- package/workers/schema.sql +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 - Connect page user context
|
|
4
|
+
|
|
5
|
+
- Added AI tool and multi-select use-case fields to the `/connect` page.
|
|
6
|
+
- Stored generated token metadata as `client` and `use_case` for later product direction and billing analysis.
|
|
7
|
+
- Added client-specific configuration hints/snippets to downloaded connect files for WorkBuddy, Claude Desktop, Claude Code, Codex, OpenCode, and generic Agents.
|
|
8
|
+
|
|
3
9
|
## 0.4.0 - Self-serve Online Gate connection
|
|
4
10
|
|
|
5
11
|
- Added `product_spec_connect` so Agents can guide users through connecting the Online PM Gate without manual token copying.
|
package/README.md
CHANGED
|
@@ -208,6 +208,8 @@ Client-specific integration notes are intentionally kept out of the main user fl
|
|
|
208
208
|
"client": "workbuddy",
|
|
209
209
|
"connect_file": {
|
|
210
210
|
"type": "product-spec-mcp-connect",
|
|
211
|
+
"client": "workbuddy",
|
|
212
|
+
"useCases": ["personal_app_site", "client_requirements"],
|
|
211
213
|
"instructions": {
|
|
212
214
|
"env": {
|
|
213
215
|
"PRODUCT_SPEC_REMOTE_GATE_URL": "https://productmcp.opc-mind.top/v1/pm-intent",
|
package/dist/index.cjs
CHANGED
|
@@ -30154,7 +30154,7 @@ function formatConnectGuide(result) {
|
|
|
30154
30154
|
function createServer() {
|
|
30155
30155
|
const server = new McpServer({
|
|
30156
30156
|
name: "product-spec-mcp",
|
|
30157
|
-
version: "0.4.
|
|
30157
|
+
version: "0.4.1"
|
|
30158
30158
|
});
|
|
30159
30159
|
registerSpecInterrogate(server);
|
|
30160
30160
|
registerSpecCompile(server);
|
package/docs/connect-flow.md
CHANGED
|
@@ -31,7 +31,12 @@ https://productmcp.opc-mind.top/connect
|
|
|
31
31
|
POST /v1/connect-token
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
页面会让用户选择两个轻量字段:
|
|
35
|
+
|
|
36
|
+
- 正在用哪个 AI 工具:WorkBuddy / Claude Desktop / Claude Code / Codex / OpenCode / 其他(请填写)
|
|
37
|
+
- 准备用它做什么:给自己做小应用 / 网站、帮客户梳理需求、公司 / 团队内部项目、学习或测试 MCP、其他(请填写),支持多选
|
|
38
|
+
|
|
39
|
+
Worker 会创建一个 `psm_` 开头的专属 token,保存这两个字段,并返回连接文件。
|
|
35
40
|
|
|
36
41
|
## Connect File
|
|
37
42
|
|
|
@@ -47,6 +52,10 @@ product-spec-mcp-connect.json
|
|
|
47
52
|
{
|
|
48
53
|
"type": "product-spec-mcp-connect",
|
|
49
54
|
"version": 1,
|
|
55
|
+
"client": "workbuddy",
|
|
56
|
+
"clientKey": "workbuddy",
|
|
57
|
+
"useCase": "personal_app_site,client_requirements",
|
|
58
|
+
"useCases": ["personal_app_site", "client_requirements"],
|
|
50
59
|
"remoteGate": {
|
|
51
60
|
"url": "https://productmcp.opc-mind.top/v1/pm-intent",
|
|
52
61
|
"token": "psm_xxx",
|
|
@@ -62,6 +71,13 @@ product-spec-mcp-connect.json
|
|
|
62
71
|
"PRODUCT_SPEC_REMOTE_GATE_MODE": "auto",
|
|
63
72
|
"PRODUCT_SPEC_REMOTE_GATE_TIMEOUT_MS": "10000",
|
|
64
73
|
"PRODUCT_SPEC_TELEMETRY": "off"
|
|
74
|
+
},
|
|
75
|
+
"clientHint": {
|
|
76
|
+
"target": "WorkBuddy MCP wrapper",
|
|
77
|
+
"action": "把 instructions.env 写入 WorkBuddy 中 product-spec MCP server 的环境变量配置,然后重启 MCP。"
|
|
78
|
+
},
|
|
79
|
+
"configSnippet": {
|
|
80
|
+
"env": {}
|
|
65
81
|
}
|
|
66
82
|
}
|
|
67
83
|
}
|
|
@@ -122,6 +138,15 @@ usage_events
|
|
|
122
138
|
|
|
123
139
|
`api_tokens` 存 token hash,不存明文 token。`usage_events` 记录按 token 的 LLM 使用情况,后续可以接入计费、月额度、封禁和用户面板。
|
|
124
140
|
|
|
141
|
+
`api_tokens` 还会保存:
|
|
142
|
+
|
|
143
|
+
```sql
|
|
144
|
+
client TEXT,
|
|
145
|
+
use_case TEXT
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
这两个字段用于判断真实用户分布和后续生成更准确的 Agent 配置提示。
|
|
149
|
+
|
|
125
150
|
## Quotas
|
|
126
151
|
|
|
127
152
|
默认每日额度仍由 Worker 变量控制:
|
package/docs/online-pm-gate.md
CHANGED
|
@@ -93,6 +93,8 @@ POST /v1/connect-token
|
|
|
93
93
|
|
|
94
94
|
用户打开 `/connect` 后点击下载 `product-spec-mcp-connect.json`。文件里包含当前 Agent 应写入 MCP 配置的环境变量:
|
|
95
95
|
|
|
96
|
+
页面会额外收集两个轻量字段:当前 AI 工具和主要用途。AI 工具选项是 WorkBuddy / Claude Desktop / Claude Code / Codex / OpenCode / 其他;主要用途支持多选,包括给自己做小应用 / 网站、帮客户梳理需求、公司 / 团队内部项目、学习或测试 MCP、其他。它们会写入 D1 的 token metadata,用于后续产品方向分析和生成更准确的配置片段。
|
|
97
|
+
|
|
96
98
|
```json
|
|
97
99
|
{
|
|
98
100
|
"PRODUCT_SPEC_REMOTE_GATE_URL": "https://productmcp.opc-mind.top/v1/pm-intent",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-spec-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "MCP Server for product specification - requirement interrogation, architecture decision, UI translation, debug guidance, and acceptance generation",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"docs/connect-flow.md",
|
|
15
15
|
"docs/online-pm-gate.md",
|
|
16
16
|
"workers/pm-intent-gate.mjs",
|
|
17
|
+
"workers/migrations/*.sql",
|
|
17
18
|
"workers/schema.sql",
|
|
18
19
|
"workers/wrangler.toml.example"
|
|
19
20
|
],
|
|
@@ -281,35 +281,45 @@ async function createConnectToken(request, env, origin) {
|
|
|
281
281
|
const now = new Date().toISOString();
|
|
282
282
|
const dailyLimit = resolveConnectTokenDailyLimit(env);
|
|
283
283
|
const monthlyLimit = positiveIntegerOrNull(env.CONNECT_TOKEN_MONTHLY_LIMIT);
|
|
284
|
-
const
|
|
284
|
+
const clientKey = normalizeSubmittedClient(body.client);
|
|
285
|
+
const clientOther = sanitizeOptionalText(body.clientOther || body.client_other, 60);
|
|
286
|
+
const client = clientKey === "other" && clientOther ? `other:${clientOther}` : clientKey;
|
|
287
|
+
const useCases = normalizeSubmittedUseCases(body);
|
|
288
|
+
const useCase = useCases.length > 0 ? useCases.join(",") : "unknown";
|
|
285
289
|
const label = sanitizeShortText(body.label || `${client} connect token`, 80);
|
|
286
290
|
|
|
287
291
|
await env.PROMPT_SAMPLES.prepare(
|
|
288
292
|
`INSERT INTO api_tokens (
|
|
289
|
-
id, token_hash, token_prefix, label, daily_limit, monthly_limit, enabled, created_at, last_used_at
|
|
290
|
-
) VALUES (?, ?, ?, ?, ?, ?, 1, ?, NULL)`
|
|
293
|
+
id, token_hash, token_prefix, label, client, use_case, daily_limit, monthly_limit, enabled, created_at, last_used_at
|
|
294
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, NULL)`
|
|
291
295
|
).bind(
|
|
292
296
|
id,
|
|
293
297
|
tokenHash,
|
|
294
298
|
token.slice(0, 12),
|
|
295
299
|
label,
|
|
300
|
+
client,
|
|
301
|
+
useCase,
|
|
296
302
|
dailyLimit,
|
|
297
303
|
monthlyLimit,
|
|
298
304
|
now
|
|
299
305
|
).run();
|
|
300
306
|
|
|
301
307
|
const remoteGateUrl = resolveRemoteGateUrl(env, origin);
|
|
302
|
-
const connectFile = buildConnectFile(remoteGateUrl, token);
|
|
308
|
+
const connectFile = buildConnectFile(remoteGateUrl, token, clientKey, client, useCases);
|
|
303
309
|
return json({
|
|
304
310
|
ok: true,
|
|
305
311
|
tokenPrefix: token.slice(0, 12),
|
|
306
312
|
dailyLimit,
|
|
307
313
|
monthlyLimit,
|
|
314
|
+
client,
|
|
315
|
+
clientKey,
|
|
316
|
+
useCase,
|
|
317
|
+
useCases,
|
|
308
318
|
connectFile,
|
|
309
319
|
});
|
|
310
320
|
}
|
|
311
321
|
|
|
312
|
-
function buildConnectFile(remoteGateUrl, token) {
|
|
322
|
+
function buildConnectFile(remoteGateUrl, token, clientKey = "other", client = "other", useCases = []) {
|
|
313
323
|
const env = {
|
|
314
324
|
PRODUCT_SPEC_REMOTE_GATE_URL: remoteGateUrl,
|
|
315
325
|
PRODUCT_SPEC_REMOTE_GATE_TOKEN: token,
|
|
@@ -320,6 +330,10 @@ function buildConnectFile(remoteGateUrl, token) {
|
|
|
320
330
|
return {
|
|
321
331
|
type: "product-spec-mcp-connect",
|
|
322
332
|
version: 1,
|
|
333
|
+
client,
|
|
334
|
+
clientKey,
|
|
335
|
+
useCase: useCases.join(",") || "unknown",
|
|
336
|
+
useCases,
|
|
323
337
|
remoteGate: {
|
|
324
338
|
url: remoteGateUrl,
|
|
325
339
|
token,
|
|
@@ -330,10 +344,115 @@ function buildConnectFile(remoteGateUrl, token) {
|
|
|
330
344
|
instructions: {
|
|
331
345
|
summary: "请把 remoteGate 配置写入当前 Agent 的 product-spec-mcp 环境变量。",
|
|
332
346
|
env,
|
|
347
|
+
clientHint: clientConnectHint(clientKey),
|
|
348
|
+
configSnippet: clientConfigSnippet(clientKey, env),
|
|
333
349
|
},
|
|
334
350
|
};
|
|
335
351
|
}
|
|
336
352
|
|
|
353
|
+
function normalizeSubmittedClient(value) {
|
|
354
|
+
const normalized = normalizeClient(value);
|
|
355
|
+
return ["workbuddy", "claude_desktop", "claude_code", "codex", "opencode", "other"].includes(normalized)
|
|
356
|
+
? normalized
|
|
357
|
+
: "other";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeSubmittedUseCases(body) {
|
|
361
|
+
const rawValues = Array.isArray(body.useCases)
|
|
362
|
+
? body.useCases
|
|
363
|
+
: String(body.useCase || body.use_case || "")
|
|
364
|
+
.split(",")
|
|
365
|
+
.map((item) => item.trim())
|
|
366
|
+
.filter(Boolean);
|
|
367
|
+
const allowed = new Set(["personal_app_site", "client_requirements", "internal_team", "learning_testing", "other"]);
|
|
368
|
+
const normalized = [];
|
|
369
|
+
for (const value of rawValues) {
|
|
370
|
+
const key = String(value || "").trim();
|
|
371
|
+
if (allowed.has(key) && !normalized.includes(key)) normalized.push(key);
|
|
372
|
+
}
|
|
373
|
+
const otherText = sanitizeOptionalText(body.useCaseOther || body.use_case_other, 80);
|
|
374
|
+
if (normalized.includes("other") && otherText) {
|
|
375
|
+
normalized[normalized.indexOf("other")] = `other:${otherText}`;
|
|
376
|
+
}
|
|
377
|
+
return normalized;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function clientConnectHint(client) {
|
|
381
|
+
const normalized = normalizeClient(client);
|
|
382
|
+
const hints = {
|
|
383
|
+
workbuddy: {
|
|
384
|
+
target: "WorkBuddy MCP wrapper",
|
|
385
|
+
action: "把 instructions.env 写入 WorkBuddy 中 product-spec MCP server 的环境变量配置,然后重启 MCP。",
|
|
386
|
+
},
|
|
387
|
+
cursor: {
|
|
388
|
+
target: "Cursor MCP configuration",
|
|
389
|
+
action: "把 configSnippet 合并到 Cursor 的 MCP 配置;如果已有 product-spec server,只补 env。",
|
|
390
|
+
},
|
|
391
|
+
claude_desktop: {
|
|
392
|
+
target: "Claude Desktop MCP configuration",
|
|
393
|
+
action: "把 configSnippet 合并到 claude_desktop_config.json;如果已有 product-spec server,只补 env。",
|
|
394
|
+
},
|
|
395
|
+
claude_code: {
|
|
396
|
+
target: "Claude Code MCP configuration",
|
|
397
|
+
action: "把 instructions.env 写入 Claude Code 中 product-spec MCP server 的环境变量配置,然后重启 MCP。",
|
|
398
|
+
},
|
|
399
|
+
codex: {
|
|
400
|
+
target: "Codex MCP configuration",
|
|
401
|
+
action: "把 instructions.env 写入当前 Codex 可用的 product-spec MCP server 配置,然后重启 MCP。",
|
|
402
|
+
},
|
|
403
|
+
opencode: {
|
|
404
|
+
target: "opencode MCP configuration",
|
|
405
|
+
action: "把 configSnippet 合并到 ~/.config/opencode/opencode.json;如果已有 product-spec server,只补 env。",
|
|
406
|
+
},
|
|
407
|
+
other: {
|
|
408
|
+
target: "Generic MCP server configuration",
|
|
409
|
+
action: "把 instructions.env 写入 product-spec MCP server 的环境变量配置,然后重启 MCP。",
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
return hints[normalized] || hints.other;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function clientConfigSnippet(client, env) {
|
|
416
|
+
const normalized = normalizeClient(client);
|
|
417
|
+
if (normalized === "opencode") {
|
|
418
|
+
return {
|
|
419
|
+
mcp: {
|
|
420
|
+
"product-spec": {
|
|
421
|
+
type: "local",
|
|
422
|
+
command: ["npx", "-y", "product-spec-mcp@latest"],
|
|
423
|
+
enabled: true,
|
|
424
|
+
timeout: 30000,
|
|
425
|
+
env,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (normalized === "cursor" || normalized === "claude_desktop") {
|
|
431
|
+
return {
|
|
432
|
+
mcpServers: {
|
|
433
|
+
"product-spec": {
|
|
434
|
+
command: "npx",
|
|
435
|
+
args: ["-y", "product-spec-mcp@latest"],
|
|
436
|
+
env,
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return { env };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function normalizeClient(client) {
|
|
445
|
+
const value = String(client || "").toLowerCase().replace(/[\s-]+/g, "_");
|
|
446
|
+
if (value.includes("workbuddy")) return "workbuddy";
|
|
447
|
+
if (value.includes("cursor")) return "cursor";
|
|
448
|
+
if (value.includes("claude_code")) return "claude_code";
|
|
449
|
+
if (value.includes("claude") && value.includes("code")) return "claude_code";
|
|
450
|
+
if (value.includes("claude")) return "claude_desktop";
|
|
451
|
+
if (value.includes("codex")) return "codex";
|
|
452
|
+
if (value.includes("opencode")) return "opencode";
|
|
453
|
+
return ["workbuddy", "cursor", "claude_desktop", "claude_code", "codex", "opencode", "other"].includes(value) ? value : "other";
|
|
454
|
+
}
|
|
455
|
+
|
|
337
456
|
function connectPageHtml(origin) {
|
|
338
457
|
const apiUrl = `${origin}/v1/connect-token`;
|
|
339
458
|
return `<!doctype html>
|
|
@@ -352,6 +471,15 @@ function connectPageHtml(origin) {
|
|
|
352
471
|
.steps { display: grid; gap: 12px; margin: 26px 0; padding: 0; list-style: none; }
|
|
353
472
|
.steps li { display: flex; gap: 12px; align-items: flex-start; color: #243246; }
|
|
354
473
|
.num { flex: 0 0 28px; height: 28px; border-radius: 999px; background: #0f766e; color: white; display: grid; place-items: center; font-weight: 700; font-size: 14px; }
|
|
474
|
+
.form { display: grid; gap: 18px; margin: 24px 0; }
|
|
475
|
+
label { display: grid; gap: 8px; font-size: 14px; font-weight: 700; color: #243246; }
|
|
476
|
+
select, input[type="text"] { width: 100%; box-sizing: border-box; border: 1px solid #d8e0eb; border-radius: 8px; background: white; color: #172033; font: inherit; padding: 12px 14px; }
|
|
477
|
+
select:focus, input[type="text"]:focus { outline: 2px solid rgba(15, 118, 110, .24); border-color: #0f766e; }
|
|
478
|
+
.hint { font-size: 13px; font-weight: 500; color: #7a8698; }
|
|
479
|
+
.hidden { display: none; }
|
|
480
|
+
.checks { display: grid; gap: 10px; }
|
|
481
|
+
.check { display: flex; gap: 10px; align-items: center; font-size: 15px; font-weight: 600; color: #243246; }
|
|
482
|
+
.check input { width: 18px; height: 18px; accent-color: #0f766e; }
|
|
355
483
|
button { appearance: none; border: 0; border-radius: 8px; background: #0f766e; color: white; font-size: 16px; font-weight: 700; padding: 14px 18px; cursor: pointer; }
|
|
356
484
|
button:disabled { opacity: .65; cursor: wait; }
|
|
357
485
|
.status { margin-top: 16px; font-size: 14px; color: #526071; min-height: 22px; }
|
|
@@ -368,6 +496,33 @@ function connectPageHtml(origin) {
|
|
|
368
496
|
<li><span class="num">2</span><span>把下载的 <strong>product-spec-mcp-connect.json</strong> 拖回或上传到 Agent 对话。</span></li>
|
|
369
497
|
<li><span class="num">3</span><span>让 Agent 按文件里的说明完成配置并重启 MCP。</span></li>
|
|
370
498
|
</ol>
|
|
499
|
+
<div class="form">
|
|
500
|
+
<label>
|
|
501
|
+
你正在用哪个 AI 工具?
|
|
502
|
+
<span class="hint">用于生成更准确的配置说明</span>
|
|
503
|
+
<select id="client">
|
|
504
|
+
<option value="workbuddy">WorkBuddy</option>
|
|
505
|
+
<option value="claude_desktop">Claude Desktop</option>
|
|
506
|
+
<option value="claude_code">Claude Code</option>
|
|
507
|
+
<option value="codex">Codex</option>
|
|
508
|
+
<option value="opencode">OpenCode</option>
|
|
509
|
+
<option value="other">其他</option>
|
|
510
|
+
</select>
|
|
511
|
+
<input id="clientOther" class="hidden" type="text" maxlength="60" placeholder="请填写 AI 工具名称">
|
|
512
|
+
</label>
|
|
513
|
+
<label>
|
|
514
|
+
你准备用它做什么?
|
|
515
|
+
<span class="hint">可多选,只用于改进 product-spec,不影响连接</span>
|
|
516
|
+
<span class="checks" id="useCases">
|
|
517
|
+
<label class="check"><input type="checkbox" value="personal_app_site" checked>给自己做小应用 / 网站</label>
|
|
518
|
+
<label class="check"><input type="checkbox" value="client_requirements">帮客户梳理需求</label>
|
|
519
|
+
<label class="check"><input type="checkbox" value="internal_team">公司 / 团队内部项目</label>
|
|
520
|
+
<label class="check"><input type="checkbox" value="learning_testing">学习或测试 MCP</label>
|
|
521
|
+
<label class="check"><input id="useCaseOtherCheck" type="checkbox" value="other">其他</label>
|
|
522
|
+
</span>
|
|
523
|
+
<input id="useCaseOther" class="hidden" type="text" maxlength="80" placeholder="请填写其他用途">
|
|
524
|
+
</label>
|
|
525
|
+
</div>
|
|
371
526
|
<button id="download">生成并下载连接文件</button>
|
|
372
527
|
<div class="status" id="status"></div>
|
|
373
528
|
<p class="fine">连接文件里包含你的专属访问 token,请不要公开分享。默认额度由服务端配置控制。</p>
|
|
@@ -376,14 +531,31 @@ function connectPageHtml(origin) {
|
|
|
376
531
|
<script>
|
|
377
532
|
const button = document.getElementById("download");
|
|
378
533
|
const status = document.getElementById("status");
|
|
534
|
+
const client = document.getElementById("client");
|
|
535
|
+
const clientOther = document.getElementById("clientOther");
|
|
536
|
+
const useCaseOtherCheck = document.getElementById("useCaseOtherCheck");
|
|
537
|
+
const useCaseOther = document.getElementById("useCaseOther");
|
|
538
|
+
client.addEventListener("change", () => {
|
|
539
|
+
clientOther.classList.toggle("hidden", client.value !== "other");
|
|
540
|
+
});
|
|
541
|
+
useCaseOtherCheck.addEventListener("change", () => {
|
|
542
|
+
useCaseOther.classList.toggle("hidden", !useCaseOtherCheck.checked);
|
|
543
|
+
});
|
|
379
544
|
button.addEventListener("click", async () => {
|
|
380
545
|
button.disabled = true;
|
|
381
546
|
status.textContent = "正在生成连接文件...";
|
|
382
547
|
try {
|
|
548
|
+
const useCases = Array.from(document.querySelectorAll("#useCases input[type='checkbox']:checked"))
|
|
549
|
+
.map((item) => item.value);
|
|
383
550
|
const response = await fetch(${JSON.stringify(apiUrl)}, {
|
|
384
551
|
method: "POST",
|
|
385
552
|
headers: { "content-type": "application/json" },
|
|
386
|
-
body: JSON.stringify({
|
|
553
|
+
body: JSON.stringify({
|
|
554
|
+
client: client.value,
|
|
555
|
+
clientOther: clientOther.value,
|
|
556
|
+
useCases,
|
|
557
|
+
useCaseOther: useCaseOther.value
|
|
558
|
+
})
|
|
387
559
|
});
|
|
388
560
|
const payload = await response.json();
|
|
389
561
|
if (!response.ok || !payload.connectFile) throw new Error(payload.error || "connect_failed");
|
|
@@ -426,11 +598,18 @@ function positiveIntegerOrNull(value) {
|
|
|
426
598
|
|
|
427
599
|
function sanitizeShortText(value, maxLength) {
|
|
428
600
|
return String(value || "")
|
|
429
|
-
.replace(/[^\
|
|
601
|
+
.replace(/[^\p{L}\p{N}\s.@:_-]/gu, "")
|
|
430
602
|
.trim()
|
|
431
603
|
.slice(0, maxLength) || "product-spec-mcp";
|
|
432
604
|
}
|
|
433
605
|
|
|
606
|
+
function sanitizeOptionalText(value, maxLength) {
|
|
607
|
+
return String(value || "")
|
|
608
|
+
.replace(/[^\p{L}\p{N}\s.@:_-]/gu, "")
|
|
609
|
+
.trim()
|
|
610
|
+
.slice(0, maxLength);
|
|
611
|
+
}
|
|
612
|
+
|
|
434
613
|
async function callOpenAiCompatible(llm, prompt) {
|
|
435
614
|
if (!llm.apiKey) throw new Error(`missing_${llm.provider}_api_key`);
|
|
436
615
|
const response = await fetch(`${normalizeBaseUrl(llm.baseUrl)}/chat/completions`, {
|
package/workers/schema.sql
CHANGED