product-spec-mcp 0.4.1 → 0.4.2
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 +8 -0
- package/README.md +32 -8
- package/dist/index.cjs +126 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 - MVP readiness fixes
|
|
4
|
+
|
|
5
|
+
- Added a generic backend order-workflow compile path for QR ordering and kitchen status scenarios.
|
|
6
|
+
- Tightened architecture consistency so compiled registration features are not downgraded to frontend-only local storage.
|
|
7
|
+
- Improved AA calculator local specs with participants, payments, settlements, and algorithm acceptance checks.
|
|
8
|
+
- Marked invalid `product_spec_connect` files in structured output and kept the specific validation warnings visible.
|
|
9
|
+
- Rewrote the README opening around users who need help turning product ideas into executable engineering specs.
|
|
10
|
+
|
|
3
11
|
## 0.4.1 - Connect page user context
|
|
4
12
|
|
|
5
13
|
- Added AI tool and multi-select use-case fields to the `/connect` page.
|
package/README.md
CHANGED
|
@@ -1,14 +1,40 @@
|
|
|
1
1
|
# product-spec-mcp
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 把一句模糊的产品想法,整理成 AI Agent 可以执行的工程规格。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
很多人能说清“我想做什么”,但还没法直接写出功能范围、数据字段、架构边界和验收标准。`product-spec-mcp` 先帮你把想法过一遍产品经理式需求闸门,再交给 Codex、Claude、Cursor、OpenCode 等 Agent 开始写代码。
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 适合谁
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- 你有一个小应用、网站、工具或 SaaS 想法,但不知道怎样拆成开发规格。
|
|
10
|
+
- 你是创始人、运营、设计师、小团队负责人、学生,或刚开始用 AI 写代码的人。
|
|
11
|
+
- 你希望 Agent 少脑补,先确认对象、字段、权限、接口、风险和验收标准。
|
|
12
|
+
- 你要判断一个需求第一版该做纯前端、本地存储、轻后端,还是完整 SaaS。
|
|
10
13
|
|
|
11
|
-
##
|
|
14
|
+
## 它会产出什么
|
|
15
|
+
|
|
16
|
+
- 追问清单:先问真正会影响实现的缺口,不套固定模板。
|
|
17
|
+
- 可执行规格:核心功能、数据模型、API 设计、非目标、风险边界。
|
|
18
|
+
- 架构建议:判断是否需要后端、登录、后台、数据库、支付或 AI Key 保护。
|
|
19
|
+
- 验收标准:把“做完了”变成可以检查的列表。
|
|
20
|
+
|
|
21
|
+
## 最短使用路径
|
|
22
|
+
|
|
23
|
+
如果不确定从哪个工具开始,直接让 Agent 调用:
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
product_spec_assist
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
输入你的原话,例如:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
我想做一个活动报名系统,用户填姓名电话报名人数,后台能查看、搜索和导出 Excel。
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
它会自动判断该追问、编译规格、给架构建议,还是生成验收标准。
|
|
36
|
+
|
|
37
|
+
需要完整开发前规格时,推荐流程是:
|
|
12
38
|
|
|
13
39
|
```
|
|
14
40
|
1. spec_interrogate → 评估需求完整度,生成追问清单
|
|
@@ -17,9 +43,7 @@
|
|
|
17
43
|
4. acceptance_generate → 生成验收标准
|
|
18
44
|
```
|
|
19
45
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
**想启用在线 PM Gate?** 先用 `product_spec_connect`,它会引导用户打开连接页、下载连接文件,并让当前 Agent 把 token 写入 MCP 配置。
|
|
46
|
+
**在线 PM Gate 是可选增强。** 默认本地规则已经可用;如果想让低置信或冲突需求走在线 LLM 辅助归门,再调用 `product_spec_connect` 下载连接文件并交给当前 Agent 配置。
|
|
23
47
|
|
|
24
48
|
## Features
|
|
25
49
|
|
package/dist/index.cjs
CHANGED
|
@@ -21334,7 +21334,7 @@ function classifyProductDomain(rawIdea, context = {}) {
|
|
|
21334
21334
|
scores[domain2] += keyHits * 6;
|
|
21335
21335
|
}
|
|
21336
21336
|
}
|
|
21337
|
-
const registration = hasAny(text, ["\u6D3B\u52A8\u62A5\u540D", "\u62A5\u540D\u7CFB\u7EDF", "\u62A5\u540D\u8868", "\u53C2\u4F1A", "\u53C2\u8D5B"]) && !negatedRegistration;
|
|
21337
|
+
const registration = hasAny(text, ["\u6D3B\u52A8\u62A5\u540D", "\u62A5\u540D\u7CFB\u7EDF", "\u62A5\u540D\u8868", "\u62A5\u540D\u7528\u6237", "\u62A5\u540D\u6570\u636E", "\u62A5\u540D\u5217\u8868", "\u7528\u6237\u62A5\u540D\u8868\u5355", "\u53C2\u4F1A", "\u53C2\u8D5B"]) && !negatedRegistration;
|
|
21338
21338
|
if (registration) scores.registration += 7;
|
|
21339
21339
|
if (registration && hasAny(text, ["\u5BFC\u51FA", "Excel", "\u624B\u673A\u53F7", "\u62A5\u540D\u5217\u8868"])) scores.registration += 3;
|
|
21340
21340
|
const commerce = hasAny(text, ["\u6570\u5B57\u8D44\u6599", "\u8D44\u6599\u5305", "\u552E\u5356", "\u8D2D\u4E70", "\u4E0B\u5355", "\u8BA2\u5355", "\u5546\u54C1", "\u4EF7\u683C"]);
|
|
@@ -21451,7 +21451,7 @@ function isPlantCareTool(text) {
|
|
|
21451
21451
|
return hasAny(text, ["\u690D\u7269", "\u6D47\u6C34", "\u65BD\u80A5", "\u517B\u62A4"]) && hasAny(text, ["\u63D0\u9192", "\u5468\u671F", "\u4ECA\u5929", "\u5F85\u529E"]);
|
|
21452
21452
|
}
|
|
21453
21453
|
function hasNegatedRegistration(text) {
|
|
21454
|
-
return /(不接|不做|不用|无需|不需要|暂不|先不|不是|不要).{0,
|
|
21454
|
+
return /(不接|不做|不用|无需|不需要|暂不|先不|不是|不要).{0,4}(报名|报名系统|报名表|手机号|导出 Excel|导出Excel)/.test(text);
|
|
21455
21455
|
}
|
|
21456
21456
|
function hasNegatedTicket(text) {
|
|
21457
21457
|
return /(不接|不做|不用|无需|不需要|暂不|先不|不是|不要).{0,8}(工单|ticket|任务协作|分派|处理人)/i.test(text);
|
|
@@ -22117,6 +22117,7 @@ function buildTechnicalProfile(rawText, context = {}) {
|
|
|
22117
22117
|
if (localTool) evidence.push("\u9700\u6C42\u50CF\u4E2A\u4EBA\u6E05\u5355\u3001\u8BB0\u5F55\u3001\u53F0\u8D26\u3001\u63D0\u9192\u6216\u6536\u85CF\u5DE5\u5177");
|
|
22118
22118
|
if (aiRisk) blockers.push("\u6D89\u53CA AI \u6216\u7B2C\u4E09\u65B9\u6A21\u578B\u5BC6\u94A5\uFF0C\u9700\u8981\u540E\u7AEF\u4FDD\u62A4\u5BC6\u94A5");
|
|
22119
22119
|
if (paymentRisk) blockers.push("\u6D89\u53CA\u652F\u4ED8\u3001\u8BA2\u5355\u6216\u6536\u6B3E\uFF0C\u9700\u8981\u540E\u7AEF\u786E\u8BA4\u91D1\u989D\u548C\u72B6\u6001");
|
|
22120
|
+
if (hasOperationalOrderWorkflowSignal(text)) evidence.push("\u9700\u6C42\u5305\u542B\u4E0B\u5355\u3001\u8BA2\u5355\u72B6\u6001\u6216\u540E\u53F0\u64CD\u4F5C\u6D41\uFF0C\u9700\u8981\u7EDF\u4E00\u540E\u7AEF\u72B6\u6001\u6E90");
|
|
22120
22121
|
if (fullBackend && !isNegatedBackend(text)) {
|
|
22121
22122
|
return profile("full_backend_saas", "high", false, true, true, true, "postgresql", evidence, blockers);
|
|
22122
22123
|
}
|
|
@@ -22241,7 +22242,11 @@ function hasStaticDisplaySignal(text) {
|
|
|
22241
22242
|
}
|
|
22242
22243
|
function hasLightBackendSignal(text, context) {
|
|
22243
22244
|
if (context.has_auth === true || context.need_backend === true || context.backend_need === true) return true;
|
|
22244
|
-
return /(后台|管理员|登录|注册|多人|团队|审核|权限|服务端|服务器|数据库|报名|预约|容量|满员|提交到服务器|公开给用户)/.test(text) && !isNegatedBackend(text);
|
|
22245
|
+
return /(后台|管理员|登录|注册|多人|团队|审核|权限|服务端|服务器|数据库|报名|预约|容量|满员|提交到服务器|公开给用户)/.test(text) && !isNegatedBackend(text) || hasOperationalOrderWorkflowSignal(text);
|
|
22246
|
+
}
|
|
22247
|
+
function hasOperationalOrderWorkflowSignal(text) {
|
|
22248
|
+
if (/(不接|不做|不用|无需|不需要|暂不|先不).{0,8}(订单|下单|后厨|扫码点餐|菜品)/.test(text)) return false;
|
|
22249
|
+
return /(扫码点餐|扫码下单|后厨|菜品|菜单|桌号|订单状态|维护菜品|顾客.{0,6}下单|下单.{0,12}(后厨|订单状态)|后厨.{0,12}(订单|状态))/.test(text);
|
|
22245
22250
|
}
|
|
22246
22251
|
function hasSaasSignal(text, context) {
|
|
22247
22252
|
return context.commercial_intent === true || /(SaaS|商业化|套餐|订阅收费|按次数|扣次|余额|多租户)/i.test(text);
|
|
@@ -22253,9 +22258,13 @@ function hasAiRisk(text, context) {
|
|
|
22253
22258
|
}
|
|
22254
22259
|
function hasPaymentRisk(text, context) {
|
|
22255
22260
|
if (context.has_payment === true) return true;
|
|
22261
|
+
if (isSplitBillAccountingText(text)) return false;
|
|
22256
22262
|
if (/(不接|不做|不用|无需|不需要|暂不|先不).{0,8}(支付|付款|收费|订单|收款|购买)/.test(text)) return false;
|
|
22257
22263
|
return /(支付|付款|收款|退款|微信支付|支付宝|在线支付|订单支付|付费套餐|套餐购买)/.test(text);
|
|
22258
22264
|
}
|
|
22265
|
+
function isSplitBillAccountingText(text) {
|
|
22266
|
+
return /(AA|aa|分账|均摊|人均|谁该转给谁|转给谁|转账建议)/.test(text) && /(记账|付款记录|付了多少钱|参与人|本地保存|localStorage|浏览器)/.test(text);
|
|
22267
|
+
}
|
|
22259
22268
|
function hasStrongBackendSignal(text, context) {
|
|
22260
22269
|
return hasAiRisk(text, context) || hasPaymentRisk(text, context) || hasLightBackendSignal(text, context);
|
|
22261
22270
|
}
|
|
@@ -24141,6 +24150,7 @@ function buildLocalToolSignalProfile(text) {
|
|
|
24141
24150
|
};
|
|
24142
24151
|
}
|
|
24143
24152
|
function extractRecordObject(text) {
|
|
24153
|
+
if (isSplitBillTool(text)) return "AA \u8BB0\u8D26";
|
|
24144
24154
|
if (/药品|药箱|药/.test(text)) return "\u836F\u54C1";
|
|
24145
24155
|
const patterns = [
|
|
24146
24156
|
/(?:做一个|做个|开发一个|创建一个|想做一个|想做个)([^,。,.;;]{1,16}?)(?:管理工具|提醒工具|记录工具|清单|小工具|页面|网页|HTML)/i,
|
|
@@ -24158,6 +24168,7 @@ function cleanObjectLabel(value) {
|
|
|
24158
24168
|
return value.replace(/^(家庭|个人|家里|我的|一个|一款)/, "").replace(/(管理|提醒|记录|清单|工具|页面|网页|HTML)$/i, "").trim();
|
|
24159
24169
|
}
|
|
24160
24170
|
function buildFieldLabels(text, recordObject) {
|
|
24171
|
+
if (isSplitBillTool(text)) return ["\u53C2\u4E0E\u4EBA", "\u4ED8\u6B3E\u8BB0\u5F55", "\u5E94\u4ED8\u91D1\u989D", "\u8F6C\u8D26\u5EFA\u8BAE", "\u5907\u6CE8"];
|
|
24161
24172
|
const labels = [`${recordObject}\u540D`];
|
|
24162
24173
|
const physicalInventory = /家里有哪些|有哪些|库存|余量|剩余|补货|存放|放在/.test(text);
|
|
24163
24174
|
addIf(labels, "\u6570\u91CF/\u5E93\u5B58", physicalInventory || /数量|库存|余量|剩余|补货/.test(text));
|
|
@@ -24171,6 +24182,9 @@ function buildFieldLabels(text, recordObject) {
|
|
|
24171
24182
|
return Array.from(new Set(labels));
|
|
24172
24183
|
}
|
|
24173
24184
|
function buildFeatureHints(text, recordObject) {
|
|
24185
|
+
if (isSplitBillTool(text)) {
|
|
24186
|
+
return ["\u53C2\u4E0E\u4EBA\u7BA1\u7406", "\u4ED8\u6B3E\u8BB0\u5F55", "\u8F6C\u8D26\u5EFA\u8BAE\u8BA1\u7B97", "\u65B0\u589E/\u7F16\u8F91/\u5220\u9664"];
|
|
24187
|
+
}
|
|
24174
24188
|
const hints = [];
|
|
24175
24189
|
if (/记录|管理|保存|清单|列表/.test(text)) hints.push(`${recordObject}\u8BB0\u5F55\u7BA1\u7406`);
|
|
24176
24190
|
if (/记录|管理|保存|清单|列表/.test(text)) hints.push("\u65B0\u589E/\u7F16\u8F91/\u5220\u9664");
|
|
@@ -24182,6 +24196,13 @@ function buildFeatureHints(text, recordObject) {
|
|
|
24182
24196
|
return Array.from(new Set(hints));
|
|
24183
24197
|
}
|
|
24184
24198
|
function buildAcceptanceItems(text, recordObject, fieldLabels) {
|
|
24199
|
+
if (isSplitBillTool(text)) {
|
|
24200
|
+
return [
|
|
24201
|
+
"\u6BCF\u4E2A\u53C2\u4E0E\u4EBA\u53EF\u4EE5\u8BB0\u5F55\u59D3\u540D\u3001\u662F\u5426\u53C2\u4E0E\u672C\u6B21 AA \u548C\u4E2A\u4EBA\u5B9E\u9645\u4ED8\u6B3E\u91D1\u989D",
|
|
24202
|
+
"\u603B\u4ED8\u6B3E\u91D1\u989D\u3001\u6BCF\u4EBA\u5E94\u4ED8\u91D1\u989D\u548C\u5DEE\u989D\u8BA1\u7B97\u4E00\u81F4",
|
|
24203
|
+
"\u8F6C\u8D26\u5EFA\u8BAE\u80FD\u8BF4\u660E\u8C01\u8BE5\u8F6C\u7ED9\u8C01\u3001\u8F6C\u591A\u5C11\u94B1\uFF0C\u6240\u6709\u8F6C\u8D26\u91D1\u989D\u5408\u8BA1\u540E\u80FD\u6E05\u96F6\u5DEE\u989D"
|
|
24204
|
+
];
|
|
24205
|
+
}
|
|
24185
24206
|
const items = [];
|
|
24186
24207
|
if (/记录|管理|保存|清单|列表/.test(text)) {
|
|
24187
24208
|
items.push(`${recordObject}\u8BB0\u5F55\u80FD\u4FDD\u5B58${fieldLabels.join("\u3001")}`);
|
|
@@ -24201,6 +24222,9 @@ function buildAcceptanceItems(text, recordObject, fieldLabels) {
|
|
|
24201
24222
|
function addIf(items, label, condition) {
|
|
24202
24223
|
if (condition) items.push(label);
|
|
24203
24224
|
}
|
|
24225
|
+
function isSplitBillTool(text) {
|
|
24226
|
+
return /(AA|aa|分账|均摊|人均|谁该转给谁|转给谁|付了多少钱|付款金额)/.test(text) && /(记账|计算|小工具|本地保存|浏览器|参与人|每个人)/.test(text);
|
|
24227
|
+
}
|
|
24204
24228
|
|
|
24205
24229
|
// src/core/promptBuilder.ts
|
|
24206
24230
|
function buildSpec(rawIdea, context, readiness) {
|
|
@@ -24234,6 +24258,9 @@ function buildSpec(rawIdea, context, readiness) {
|
|
|
24234
24258
|
if (shouldUseDomainPack && classification.domain === "registration") {
|
|
24235
24259
|
return withTechnicalProfile(buildRegistrationSpec(rawIdea, normalizedContext, readiness), technicalProfile);
|
|
24236
24260
|
}
|
|
24261
|
+
if (classification.domain === "generic" && isOperationalOrderWorkflowContext(rawIdea, normalizedContext)) {
|
|
24262
|
+
return withTechnicalProfile(buildOperationalOrderWorkflowSpec(rawIdea, normalizedContext, readiness), technicalProfile);
|
|
24263
|
+
}
|
|
24237
24264
|
if (classification.domain === "generic" && shouldUsePmGateSpec(pmIntentDecision)) {
|
|
24238
24265
|
return withTechnicalProfile(buildPmGateSpec(rawIdea, normalizedContext, readiness, pmIntentDecision), technicalProfile);
|
|
24239
24266
|
}
|
|
@@ -24702,6 +24729,68 @@ function buildDigitalCommerceSpec(rawIdea, context, readiness) {
|
|
|
24702
24729
|
inputConsumption: buildInputConsumption(context, domainKeys, "digital_commerce", "high")
|
|
24703
24730
|
};
|
|
24704
24731
|
}
|
|
24732
|
+
function buildOperationalOrderWorkflowSpec(rawIdea, context, readiness) {
|
|
24733
|
+
const platform = context.platform || context.target_platform || extractPlatform(rawIdea) || "web";
|
|
24734
|
+
const database = context.database || "SQLite";
|
|
24735
|
+
const domainKeys = ["order_flow", "admin_features", "data_persistence", "target_platform"];
|
|
24736
|
+
return {
|
|
24737
|
+
readinessScore: Math.max(readiness.score, 60),
|
|
24738
|
+
readinessStatus: "Draft Ready",
|
|
24739
|
+
isActionable: false,
|
|
24740
|
+
productGoal: context.product_goal || "\u626B\u7801\u70B9\u9910\u8BA2\u5355\u7CFB\u7EDF",
|
|
24741
|
+
targetUser: context.target_user || "\u987E\u5BA2\u3001\u540E\u53A8\u4EBA\u5458\u548C\u5E97\u94FA\u7BA1\u7406\u5458",
|
|
24742
|
+
platform,
|
|
24743
|
+
coreFeatures: [
|
|
24744
|
+
"\u987E\u5BA2\u626B\u7801\u8FDB\u5165\u684C\u53F0\u70B9\u9910\u9875",
|
|
24745
|
+
"\u83DC\u5355\u548C\u83DC\u54C1\u5C55\u793A\uFF1A\u5206\u7C7B\u3001\u540D\u79F0\u3001\u4EF7\u683C\u3001\u4E0A\u4E0B\u67B6\u72B6\u6001",
|
|
24746
|
+
"\u8D2D\u7269\u8F66\u548C\u4E0B\u5355\uFF1A\u9009\u62E9\u83DC\u54C1\u3001\u6570\u91CF\u3001\u5907\u6CE8\u5E76\u521B\u5EFA\u8BA2\u5355",
|
|
24747
|
+
"\u8BA2\u5355\u72B6\u6001\u6D41\uFF1Apending -> accepted -> cooking -> ready -> completed/cancelled",
|
|
24748
|
+
"\u540E\u53A8\u8BA2\u5355\u770B\u677F\uFF1A\u67E5\u770B\u65B0\u8BA2\u5355\u5E76\u66F4\u65B0\u5236\u4F5C\u72B6\u6001",
|
|
24749
|
+
"\u8001\u677F\u540E\u53F0\uFF1A\u7EF4\u62A4\u83DC\u54C1\u3001\u5206\u7C7B\u3001\u4EF7\u683C\u548C\u4E0A\u4E0B\u67B6\u72B6\u6001"
|
|
24750
|
+
],
|
|
24751
|
+
dataModel: buildOperationalOrderDataModel(database),
|
|
24752
|
+
architecture: [
|
|
24753
|
+
`MVP \u63A8\u8350\u5355\u4F53 Web \u540E\u7AEF\u67B6\u6784\uFF1ANode.js/Express + ${database} + \u670D\u52A1\u7AEF Session\u3002`,
|
|
24754
|
+
"\u987E\u5BA2\u4E0B\u5355\u3001\u540E\u53A8\u72B6\u6001\u548C\u8001\u677F\u7EF4\u62A4\u83DC\u54C1\u90FD\u9700\u8981\u7EDF\u4E00\u540E\u7AEF\u72B6\u6001\u6E90\uFF0C\u4E0D\u80FD\u53EA\u653E\u5728\u6D4F\u89C8\u5668 localStorage\u3002",
|
|
24755
|
+
"\u4EF7\u683C\u5FC5\u987B\u4EE5\u540E\u7AEF\u83DC\u54C1\u8868\u4E3A\u51C6\uFF0C\u524D\u7AEF\u4F20\u5165\u91D1\u989D\u53EA\u80FD\u4F5C\u4E3A\u5C55\u793A\u53C2\u8003\u3002"
|
|
24756
|
+
].join("\n"),
|
|
24757
|
+
apiDesign: [
|
|
24758
|
+
"GET /api/menus - \u67E5\u8BE2\u53EF\u5C55\u793A\u83DC\u5355\u5206\u7C7B\u548C\u5DF2\u4E0A\u67B6\u83DC\u54C1",
|
|
24759
|
+
"POST /api/orders - \u987E\u5BA2\u521B\u5EFA\u8BA2\u5355\uFF1Bbody: { tableCode, items, note }\uFF1B\u540E\u7AEF\u6309\u83DC\u54C1\u8868\u8BA1\u7B97\u91D1\u989D\u5E76\u5199\u5165 pending \u72B6\u6001",
|
|
24760
|
+
"GET /api/orders/:id - \u67E5\u8BE2\u8BA2\u5355\u8BE6\u60C5\u548C\u5F53\u524D\u72B6\u6001",
|
|
24761
|
+
"GET /api/kitchen/orders?status= - \u540E\u53A8\u67E5\u8BE2\u5F85\u5904\u7406\u548C\u5236\u4F5C\u4E2D\u8BA2\u5355\uFF1B\u9700\u8981\u540E\u53A8\u6216\u7BA1\u7406\u5458\u767B\u5F55",
|
|
24762
|
+
"PATCH /api/kitchen/orders/:id/status - \u540E\u53A8\u66F4\u65B0\u8BA2\u5355\u72B6\u6001\uFF1B\u53EA\u5141\u8BB8\u6309\u72B6\u6001\u673A\u6D41\u8F6C",
|
|
24763
|
+
"POST /api/admin/login - \u8001\u677F\u6216\u7BA1\u7406\u5458\u767B\u5F55",
|
|
24764
|
+
"GET /api/admin/dishes - \u67E5\u8BE2\u83DC\u54C1\u5217\u8868",
|
|
24765
|
+
"POST /api/admin/dishes - \u65B0\u589E\u83DC\u54C1",
|
|
24766
|
+
"PATCH /api/admin/dishes/:id - \u7F16\u8F91\u83DC\u54C1\u540D\u79F0\u3001\u4EF7\u683C\u3001\u5206\u7C7B\u6216\u4E0A\u4E0B\u67B6\u72B6\u6001",
|
|
24767
|
+
"GET /api/admin/orders - \u8001\u677F\u67E5\u8BE2\u8BA2\u5355\u5217\u8868\u548C\u72B6\u6001"
|
|
24768
|
+
].join("\n"),
|
|
24769
|
+
riskBoundaries: [
|
|
24770
|
+
"\u8BA2\u5355\u91D1\u989D\u5FC5\u987B\u7531\u540E\u7AEF\u6839\u636E\u83DC\u54C1\u4EF7\u683C\u8BA1\u7B97\uFF0C\u4E0D\u80FD\u4FE1\u4EFB\u524D\u7AEF\u4F20\u5165\u91D1\u989D",
|
|
24771
|
+
"\u540E\u53A8\u72B6\u6001\u6D41\u8F6C\u5FC5\u987B\u5728\u540E\u7AEF\u6821\u9A8C\uFF0C\u907F\u514D\u8BA2\u5355\u88AB\u8DF3\u8FC7\u6216\u91CD\u590D\u5B8C\u6210",
|
|
24772
|
+
"\u8001\u677F\u540E\u53F0\u548C\u540E\u53A8\u770B\u677F\u5FC5\u987B\u670D\u52A1\u7AEF\u9274\u6743",
|
|
24773
|
+
"\u540C\u4E00\u684C\u53F0\u77ED\u65F6\u95F4\u91CD\u590D\u63D0\u4EA4\u9700\u8981\u5E42\u7B49\u6216\u660E\u786E\u9632\u91CD\u590D\u63D0\u793A"
|
|
24774
|
+
],
|
|
24775
|
+
nonGoals: [
|
|
24776
|
+
"MVP \u6682\u4E0D\u63A5\u771F\u5B9E\u652F\u4ED8",
|
|
24777
|
+
"MVP \u6682\u4E0D\u505A\u590D\u6742\u5E93\u5B58\u3001\u4F1A\u5458\u548C\u4F18\u60E0\u5238",
|
|
24778
|
+
"MVP \u6682\u4E0D\u505A\u591A\u95E8\u5E97\u548C\u590D\u6742\u6743\u9650"
|
|
24779
|
+
],
|
|
24780
|
+
successCriteria: [
|
|
24781
|
+
"\u987E\u5BA2\u626B\u7801\u540E\u53EF\u4EE5\u770B\u5230\u5DF2\u4E0A\u67B6\u83DC\u54C1\u5E76\u521B\u5EFA\u8BA2\u5355",
|
|
24782
|
+
"\u8BA2\u5355\u521B\u5EFA\u540E\uFF0C\u540E\u53A8\u770B\u677F\u80FD\u770B\u5230\u65B0\u8BA2\u5355\u548C\u83DC\u54C1\u660E\u7EC6",
|
|
24783
|
+
"\u540E\u53A8\u66F4\u65B0\u72B6\u6001\u540E\uFF0C\u987E\u5BA2\u8BA2\u5355\u8BE6\u60C5\u80FD\u770B\u5230\u6700\u65B0\u72B6\u6001",
|
|
24784
|
+
"\u8001\u677F\u53EF\u4EE5\u65B0\u589E\u3001\u7F16\u8F91\u3001\u4E0B\u67B6\u83DC\u54C1\uFF0C\u4E14\u4E0B\u67B6\u83DC\u54C1\u4E0D\u80FD\u518D\u88AB\u4E0B\u5355",
|
|
24785
|
+
"\u8BA2\u5355\u91D1\u989D\u4EE5\u540E\u7AEF\u83DC\u54C1\u4EF7\u683C\u8BA1\u7B97\uFF0C\u4FEE\u6539\u524D\u7AEF\u91D1\u989D\u4E0D\u4F1A\u5F71\u54CD\u5B9E\u9645\u8BA2\u5355\u91D1\u989D"
|
|
24786
|
+
],
|
|
24787
|
+
assumptions: [
|
|
24788
|
+
"MVP \u9ED8\u8BA4\u6BCF\u5F20\u684C\u5B50\u4F7F\u7528\u4E00\u4E2A tableCode \u6216\u4E8C\u7EF4\u7801\u53C2\u6570\u8BC6\u522B\u6765\u6E90\u3002",
|
|
24789
|
+
"MVP \u9ED8\u8BA4\u540E\u53A8\u548C\u8001\u677F\u4F7F\u7528\u7B80\u5355\u8D26\u53F7\u767B\u5F55\uFF0C\u4E0D\u505A\u590D\u6742 RBAC\u3002"
|
|
24790
|
+
],
|
|
24791
|
+
inputConsumption: buildInputConsumption(context, domainKeys, "generic", "medium")
|
|
24792
|
+
};
|
|
24793
|
+
}
|
|
24705
24794
|
function buildAppointmentSpec(rawIdea, context, readiness) {
|
|
24706
24795
|
const serviceCatalog = context.service_catalog || "\u670D\u52A1\u9879\u76EE\u5305\u542B\u540D\u79F0\u3001\u7B80\u4ECB\u3001\u65F6\u957F\u3001\u53EF\u9884\u7EA6\u72B6\u6001";
|
|
24707
24796
|
const timeSlotRule = context.time_slot_rule || "\u7BA1\u7406\u5458\u53EF\u4EE5\u8BBE\u7F6E\u65E5\u671F\u3001\u5F00\u59CB\u65F6\u95F4\u3001\u7ED3\u675F\u65F6\u95F4\u3001\u6700\u5927\u9884\u7EA6\u4EBA\u6570";
|
|
@@ -25357,6 +25446,12 @@ function impliesTrueForField(field, text) {
|
|
|
25357
25446
|
}
|
|
25358
25447
|
return false;
|
|
25359
25448
|
}
|
|
25449
|
+
function isOperationalOrderWorkflowContext(rawIdea, context) {
|
|
25450
|
+
const text = `${rawIdea} ${JSON.stringify(context)}`;
|
|
25451
|
+
if (classifyProductDomain(rawIdea, context).domain !== "generic") return false;
|
|
25452
|
+
if (/(不接|不做|不用|无需|不需要|暂不|先不).{0,8}(订单|下单|后厨|扫码点餐|菜品)/.test(text)) return false;
|
|
25453
|
+
return /(扫码点餐|扫码下单|后厨|菜品|菜单|桌号|订单状态|维护菜品|顾客.{0,6}下单|下单.{0,12}(后厨|订单状态)|后厨.{0,12}(订单|状态))/.test(text);
|
|
25454
|
+
}
|
|
25360
25455
|
function parseListLike(value, fallback) {
|
|
25361
25456
|
if (!value) return fallback;
|
|
25362
25457
|
if (Array.isArray(value)) return value.map(String).filter(Boolean);
|
|
@@ -25396,6 +25491,17 @@ function buildDigitalCommerceDataModel(database) {
|
|
|
25396
25491
|
"\u5EFA\u8BAE\u7D22\u5F15\uFF1Aidx_orders_user(user_id)\uFF0Cidx_orders_product(product_id)\uFF0Cidx_downloads_user(user_id)\uFF0Cidx_downloads_order(order_id)"
|
|
25397
25492
|
].join("\n");
|
|
25398
25493
|
}
|
|
25494
|
+
function buildOperationalOrderDataModel(database) {
|
|
25495
|
+
return [
|
|
25496
|
+
`\u6570\u636E\u5E93\uFF1A${database}`,
|
|
25497
|
+
"menus(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, sort_order INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active')",
|
|
25498
|
+
"dishes(id INTEGER PRIMARY KEY AUTOINCREMENT, menu_id INTEGER NOT NULL, name TEXT NOT NULL, description TEXT, price_cents INTEGER NOT NULL CHECK(price_cents >= 0), status TEXT NOT NULL DEFAULT 'active', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)",
|
|
25499
|
+
"orders(id INTEGER PRIMARY KEY AUTOINCREMENT, table_code TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', total_cents INTEGER NOT NULL DEFAULT 0, note TEXT DEFAULT '', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)",
|
|
25500
|
+
"order_items(id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, dish_id INTEGER NOT NULL, dish_name TEXT NOT NULL, unit_price_cents INTEGER NOT NULL, quantity INTEGER NOT NULL CHECK(quantity >= 1), note TEXT DEFAULT '')",
|
|
25501
|
+
"staff_users(id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'kitchen', created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP)",
|
|
25502
|
+
"\u5EFA\u8BAE\u7D22\u5F15\uFF1Aidx_orders_status(status)\uFF0Cidx_orders_table(table_code)\uFF0Cidx_order_items_order(order_id)\uFF0C\u7528\u4E8E\u540E\u53A8\u770B\u677F\u548C\u8BA2\u5355\u8BE6\u60C5\u67E5\u8BE2"
|
|
25503
|
+
].join("\n");
|
|
25504
|
+
}
|
|
25399
25505
|
function buildAppointmentDataModel(database) {
|
|
25400
25506
|
return [
|
|
25401
25507
|
`\u6570\u636E\u5E93\uFF1A${database}`,
|
|
@@ -25658,6 +25764,10 @@ function buildLocalRecordJsonExample(recordObject, fieldExample) {
|
|
|
25658
25764
|
};
|
|
25659
25765
|
for (const label of labels) {
|
|
25660
25766
|
if (label.includes("\u540D")) record2.name = `\u793A\u4F8B${recordObject}`;
|
|
25767
|
+
else if (label.includes("\u53C2\u4E0E\u4EBA")) record2.participants = [{ id: "p1", name: "\u5F20\u4E09" }, { id: "p2", name: "\u674E\u56DB" }];
|
|
25768
|
+
else if (label.includes("\u4ED8\u6B3E\u8BB0\u5F55")) record2.payments = [{ payerId: "p1", amount: 120, note: "\u665A\u9910" }];
|
|
25769
|
+
else if (label.includes("\u5E94\u4ED8\u91D1\u989D")) record2.shareAmount = 60;
|
|
25770
|
+
else if (label.includes("\u8F6C\u8D26\u5EFA\u8BAE")) record2.settlements = [{ from: "p2", to: "p1", amount: 60 }];
|
|
25661
25771
|
else if (label.includes("\u6570\u91CF") || label.includes("\u5E93\u5B58")) record2.quantity = 1;
|
|
25662
25772
|
else if (label.includes("\u6709\u6548\u671F") || label.includes("\u5230\u671F")) record2.expireDate = "2026-12-31";
|
|
25663
25773
|
else if (label.includes("\u5206\u7C7B")) record2.category = "\u9ED8\u8BA4\u5206\u7C7B";
|
|
@@ -26172,12 +26282,12 @@ function decideArchitecture(productType, platform, features, commercialIntent, e
|
|
|
26172
26282
|
"crm"
|
|
26173
26283
|
].includes(domain);
|
|
26174
26284
|
const isLightweightCoveredDomain = coveredOperationalDomain && expectedUsers !== "enterprise" && expectedUsers !== "massive" && !commercialIntent && !paymentRisk && !aiKeyRisk;
|
|
26175
|
-
const needsBackend = isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_backend || matchedRules.some((r) => r.result.need_backend);
|
|
26176
|
-
const needsAuth = !singleUserCrm && (isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_auth || matchedRules.some((r) => r.result.need_auth) || /登录|鉴权|权限|下载|后台|管理员|处理人|成员|销售/.test(allText));
|
|
26177
|
-
const needsAdmin = !singleUserCrm && (isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_admin || matchedRules.some((r) => r.result.need_admin) || /后台|管理员|审核|举报|隐藏|下架|分配|处理人|发布|撤回|目录权限|成员权限|负责人/.test(allText));
|
|
26285
|
+
const needsBackend = technicalProfile.needsBackend || isLightweightCoveredDomain || isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_backend || matchedRules.some((r) => r.result.need_backend);
|
|
26286
|
+
const needsAuth = !singleUserCrm && (technicalProfile.needsAuth || isLightweightCoveredDomain || isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_auth || matchedRules.some((r) => r.result.need_auth) || /登录|鉴权|权限|下载|后台|管理员|处理人|成员|销售/.test(allText));
|
|
26287
|
+
const needsAdmin = !singleUserCrm && (technicalProfile.needsAdmin || isLightweightCoveredDomain || isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || result.need_admin || matchedRules.some((r) => r.result.need_admin) || /后台|管理员|审核|举报|隐藏|下架|分配|处理人|发布|撤回|目录权限|成员权限|负责人/.test(allText));
|
|
26178
26288
|
const needsLogging = paymentRisk || aiKeyRisk || !isLightweightIndividualMvp && (result.need_logging || matchedRules.some((r) => r.result.need_logging));
|
|
26179
26289
|
return {
|
|
26180
|
-
canBeFrontendOnly: result.can_be_frontend_only,
|
|
26290
|
+
canBeFrontendOnly: needsBackend ? false : result.can_be_frontend_only,
|
|
26181
26291
|
needBackend: needsBackend,
|
|
26182
26292
|
needSeparation: isLightweightIndividualMvp || isLightweightCoveredDomain ? false : result.need_separation,
|
|
26183
26293
|
recommendedDatabase: isIndividualRegistrationMvp || isLightweightCoveredDomain && domain === "registration" ? "SQLite" : isIndividualDigitalCommerceMvp || isIndividualAppointmentMvp || isIndividualContentCommunityMvp || isIndividualTicketWorkflowMvp || isIndividualKnowledgeBaseMvp || isIndividualCrmMvp || isLightweightCoveredDomain ? "SQLite \u6216 JSON \u6587\u4EF6\u5B58\u50A8" : dbRec,
|
|
@@ -26202,6 +26312,7 @@ function hasNegatedPayment2(text) {
|
|
|
26202
26312
|
}
|
|
26203
26313
|
function hasDirectPaymentRisk(text) {
|
|
26204
26314
|
if (hasNegatedPayment2(text)) return false;
|
|
26315
|
+
if (/(AA|aa|分账|均摊|人均|谁该转给谁|转给谁|转账建议)/.test(text) && /(记账|付款记录|付了多少钱|参与人|本地保存|localStorage|浏览器)/.test(text)) return false;
|
|
26205
26316
|
return /支付|付款|收费|收款|退款|微信支付|支付宝|在线支付|付费套餐|套餐购买/.test(text);
|
|
26206
26317
|
}
|
|
26207
26318
|
|
|
@@ -28286,13 +28397,14 @@ function buildConnectGuide(connectFile, client = "unknown") {
|
|
|
28286
28397
|
return {
|
|
28287
28398
|
configured: false,
|
|
28288
28399
|
connectUrl,
|
|
28400
|
+
isError: Boolean(connectFile && parsed.warnings.length > 0),
|
|
28289
28401
|
steps: [
|
|
28290
28402
|
`\u6253\u5F00 ${connectUrl}`,
|
|
28291
28403
|
"\u70B9\u51FB\u201C\u751F\u6210\u5E76\u4E0B\u8F7D\u8FDE\u63A5\u6587\u4EF6\u201D\u3002",
|
|
28292
28404
|
"\u628A\u4E0B\u8F7D\u7684 product-spec-mcp-connect.json \u53D1\u56DE\u5F53\u524D Agent \u5BF9\u8BDD\u3002",
|
|
28293
28405
|
"\u8BA9 Agent \u8BFB\u53D6\u8FDE\u63A5\u6587\u4EF6\uFF0C\u5E76\u628A instructions.env \u5199\u5165\u5F53\u524D MCP \u914D\u7F6E\u3002"
|
|
28294
28406
|
],
|
|
28295
|
-
warnings: [
|
|
28407
|
+
warnings: parsed.warnings.length > 0 ? parsed.warnings : [
|
|
28296
28408
|
"\u4E0D\u8981\u624B\u52A8\u586B\u5199 token\uFF1B\u8FDE\u63A5\u6587\u4EF6\u4E2D\u5DF2\u7ECF\u5305\u542B\u6240\u9700\u914D\u7F6E\u3002",
|
|
28297
28409
|
"\u6D4F\u89C8\u5668\u9875\u9762\u4E0D\u80FD\u76F4\u63A5\u4FEE\u6539\u672C\u673A Agent \u914D\u7F6E\uFF0C\u9700\u8981\u628A\u8FDE\u63A5\u6587\u4EF6\u4EA4\u7ED9 Agent \u5B8C\u6210\u3002"
|
|
28298
28410
|
]
|
|
@@ -30107,16 +30219,19 @@ var ProductSpecConnectOutputSchema = external_exports.object({
|
|
|
30107
30219
|
connectUrl: external_exports.string(),
|
|
30108
30220
|
env: external_exports.record(external_exports.string()).optional(),
|
|
30109
30221
|
steps: external_exports.array(external_exports.string()),
|
|
30110
|
-
warnings: external_exports.array(external_exports.string())
|
|
30222
|
+
warnings: external_exports.array(external_exports.string()),
|
|
30223
|
+
isError: external_exports.boolean().optional()
|
|
30111
30224
|
});
|
|
30112
30225
|
|
|
30113
30226
|
// src/tools/productSpecConnect.ts
|
|
30114
30227
|
function registerProductSpecConnect(server) {
|
|
30115
30228
|
const handler = async (input) => {
|
|
30116
30229
|
const result = buildConnectGuide(input.connect_file, input.client || "unknown");
|
|
30230
|
+
const isError = Boolean(input.connect_file && !result.env && result.warnings.length > 0);
|
|
30117
30231
|
return {
|
|
30118
30232
|
content: [{ type: "text", text: formatConnectGuide(result) }],
|
|
30119
|
-
structuredContent: result
|
|
30233
|
+
structuredContent: result,
|
|
30234
|
+
isError
|
|
30120
30235
|
};
|
|
30121
30236
|
};
|
|
30122
30237
|
server.registerTool(
|
|
@@ -30154,7 +30269,7 @@ function formatConnectGuide(result) {
|
|
|
30154
30269
|
function createServer() {
|
|
30155
30270
|
const server = new McpServer({
|
|
30156
30271
|
name: "product-spec-mcp",
|
|
30157
|
-
version: "0.4.
|
|
30272
|
+
version: "0.4.2"
|
|
30158
30273
|
});
|
|
30159
30274
|
registerSpecInterrogate(server);
|
|
30160
30275
|
registerSpecCompile(server);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "product-spec-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|