@my-life-buddies/buddy-creator-skill 1.6.0 → 1.7.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/PATCHES.md +4 -0
- package/README.md +2 -2
- package/SKILL.md +3 -3
- package/assets/preview/capabilities.css +18 -0
- package/assets/preview/capabilities.js +52 -0
- package/assets/preview/index.html +2 -1
- package/assets/preview/widgets.js +27 -12
- package/package.json +1 -1
- package/references/artifact-schema.md +2 -0
- package/references/capabilities.md +34 -0
- package/references/cli-workflow.md +2 -0
- package/scripts/buddy_core.py +3 -0
- package/scripts/capabilities.py +88 -0
- package/scripts/preview.py +3 -1
- package/version.json +1 -1
package/PATCHES.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# proactive-data-panel
|
|
2
|
+
|
|
3
|
+
将 baby-grow 中已确认的只读原型布局接入正式 Booklet。新增并列页签,提醒由服务蓝图可选字段提供,平台数据来自身份匹配的 CLI 工程声明。保留旧作品的未知状态,不引入默认业务内容、授权操作或第五个创作阶段。
|
|
4
|
+
|
|
1
5
|
# 独立发布 1.6.0
|
|
2
6
|
|
|
3
7
|
合并上游 1.5.0(c9c38e9cc93a9585f64c50e3651db2b010320df0)的自适应方法访谈与原 CLI 1.4.0 平台补丁。保留动态方法目标、收口范围、初始背景、小挂件设计、完成交接和面板恢复;工作区格式仍为 buddy-creator-1。两组回归合计 86 项通过。以下记录保留原补丁历史。
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Buddy Creator Skill 1.
|
|
1
|
+
# Buddy Creator Skill 1.7.0
|
|
2
2
|
|
|
3
|
-
独立发布的 Creator 资源包。DADA 仓库负责后续维护,GitHub upstream
|
|
3
|
+
独立发布的 Creator 资源包。DADA 仓库负责后续维护,GitHub upstream 用于接收上游更新。本版增加 Booklet「主动性与数据」页:展开查看服务蓝图中的提醒约定,并同步显示当前 CLI 工程声明的数据集和用途。四册、小挂件设计与旧作品接续沿用原流程。
|
|
4
4
|
|
|
5
5
|
## CLI 使用
|
|
6
6
|
|
package/SKILL.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
name: buddy-creator
|
|
3
3
|
description: 在当前宿主主对话中引导创作者完成 Buddy 搭子的四册创作、只读小挂件清单、两视图原型与设计验收,实时同步 Booklet 页面并形成交付。用于创建或修订搭子设计,不用于扮演成品搭子或直接开发应用。
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.7.0"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
# Buddy Creator Skill 1.
|
|
8
|
+
# Buddy Creator Skill 1.7.0
|
|
9
9
|
|
|
10
10
|
由当前宿主主 agent 理解、采访、举例和撰写,随附 Python 工具保存原话、管理版本与确认并更新本地预览。Skill 执行不依赖 Node、pip、LangGraph 或独立模型账号;npm 仅用于资源分发;需要实际可用的 Python 3.9+。完整包已包含预览资源。
|
|
11
11
|
|
|
@@ -47,7 +47,7 @@ metadata:
|
|
|
47
47
|
- [定义](references/definition.md):六个目标、六章,先聊具体的人和核心任务。
|
|
48
48
|
- [知识](references/knowledge.md):来源发现、实际读取、经验及缺口,七章。
|
|
49
49
|
- [方法](references/methods.md):方法与案例数量按复杂度和覆盖缺口动态增加;Agent 判断充分或用户主动结束时收口,保留待补并确认五章。执行方法具体到行动、受阻调整和可观察结果。
|
|
50
|
-
- [服务](references/service.md)
|
|
50
|
+
- [服务](references/service.md):智能规划或逐项讨论,三阶段五动线,九章。整理蓝图时同时读取 [主动性与数据](references/capabilities.md),将已讨论的提醒规则同步到对应页签;不另开创作阶段。
|
|
51
51
|
|
|
52
52
|
先决定是否还需要问、缺什么,再想怎么说。首次进入抽象主题时给一个好回答的入口;用户主动说要讲个例子,就让他讲,不再布置一套案例要求。多问“你当时怎么回她的”“当时具体看到了什么”,少把每题都问成“你希望搭子如何……”。已说明的做法、限制和选择直接复用,只问影响当前结论的缺口。语气循循善诱、专业、口语化,不靠例行点评或语气词制造亲切感。需要时补语境,不固定开头、段落或字数;一轮只留下一个核心待答内容。假设明确标注,不替专家选答案。首次采访同时读 [对话示例](references/dialogue-examples.md),学习接话和收口,不照搬成题单。
|
|
53
53
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.capabilities-workspace{max-width:1000px;margin:0 auto;padding:40px 32px 80px;color:var(--color-ink,#203e49);font:16px/1.65 -apple-system,BlinkMacSystemFont,sans-serif}
|
|
2
|
+
.capabilities-workspace[hidden]{display:none!important}
|
|
3
|
+
.capabilities-heading h1{font-size:30px;line-height:1.3;letter-spacing:-.03em;margin:0 0 12px;font-weight:600}
|
|
4
|
+
.capabilities-heading p,.capabilities-note{color:var(--color-muted,#637985);margin:8px 0 22px}
|
|
5
|
+
.capabilities-section{margin-top:36px}
|
|
6
|
+
.capabilities-section h2{font-size:20px;font-weight:600;margin:0 0 12px}
|
|
7
|
+
.capabilities-reminder,.capabilities-dataset,.capabilities-empty{border:1px solid var(--color-rule,#dce8ed);border-radius:16px;background:var(--color-paper-raised,#fff);margin:12px 0;padding:20px 24px;overflow-wrap:anywhere}
|
|
8
|
+
.capabilities-reminder summary{display:flex;align-items:center;gap:20px;cursor:pointer;list-style:none}
|
|
9
|
+
.capabilities-reminder summary::-webkit-details-marker{display:none}
|
|
10
|
+
.capabilities-reminder summary:after{content:'+';margin-left:auto;font-size:22px;color:var(--color-muted,#637985)}
|
|
11
|
+
.capabilities-reminder[open] summary:after{content:'−'}
|
|
12
|
+
.capabilities-section h3{font-size:17px;font-weight:600;margin:0 0 4px}
|
|
13
|
+
.capabilities-reminder summary p,.capabilities-dataset p{margin:0;color:var(--color-muted,#637985);white-space:pre-wrap}
|
|
14
|
+
.capabilities-fields{display:grid;grid-template-columns:1fr 1fr;gap:24px 32px;margin:22px 0 0;padding-top:22px;border-top:1px solid var(--color-rule,#dce8ed)}
|
|
15
|
+
.capabilities-fields dt{font-size:13px;color:var(--color-muted,#637985);margin-bottom:6px}
|
|
16
|
+
.capabilities-fields dd{margin:0;white-space:pre-wrap}
|
|
17
|
+
.capabilities-workspace summary:focus-visible{outline:3px solid var(--color-accent,#087e99);outline-offset:5px;border-radius:4px}
|
|
18
|
+
@media(max-width:700px){.capabilities-workspace{padding:28px 16px 48px}.capabilities-heading h1{font-size:25px}.capabilities-fields{grid-template-columns:1fr;gap:18px}.capabilities-reminder,.capabilities-dataset,.capabilities-empty{padding:18px}}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Authored content is text, never HTML. This panel has no write or consent actions.
|
|
2
|
+
const el = (tag, text, cls) => {
|
|
3
|
+
const node = document.createElement(tag);
|
|
4
|
+
if (text !== undefined) node.textContent = text;
|
|
5
|
+
if (cls) node.className = cls;
|
|
6
|
+
return node;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function renderCapabilities(panel, data) {
|
|
10
|
+
const expanded = new Set([...panel.querySelectorAll('details[open]')].map(node => node.dataset.id));
|
|
11
|
+
panel.replaceChildren();
|
|
12
|
+
const heading = el('header', undefined, 'capabilities-heading');
|
|
13
|
+
heading.append(el('h1', '主动性与数据'), el('p', '看看搭子会在什么时候主动提醒,以及需要使用哪些平台数据。'));
|
|
14
|
+
panel.append(heading);
|
|
15
|
+
|
|
16
|
+
const reminders = data?.reminders;
|
|
17
|
+
const section = el('section', undefined, 'capabilities-section');
|
|
18
|
+
section.append(el('h2', '主动提醒'));
|
|
19
|
+
if (!reminders || ['missing', 'error'].includes(reminders.status)) {
|
|
20
|
+
section.append(el('p', reminders?.message || '正在读取主动提醒规则。', 'capabilities-note'));
|
|
21
|
+
} else {
|
|
22
|
+
section.append(el('p', reminders.status === 'confirmed'
|
|
23
|
+
? '以下为创作方案中的提醒约定;实际启用情况以 App 为准。'
|
|
24
|
+
: '以下为待确认的提醒方案,可在主对话中补充或调整。', 'capabilities-note'));
|
|
25
|
+
if (!reminders.items.length) section.append(el('p', '当前方案不安排主动提醒。', 'capabilities-empty'));
|
|
26
|
+
for (const item of reminders.items) {
|
|
27
|
+
const details = el('details', undefined, 'capabilities-reminder');
|
|
28
|
+
details.dataset.id = item.id; details.open = expanded.has(item.id);
|
|
29
|
+
const summary = el('summary');
|
|
30
|
+
const label = el('div'); label.append(el('h3', item.name), el('p', item.purpose));
|
|
31
|
+
summary.append(label); details.append(summary);
|
|
32
|
+
const fields = el('dl', undefined, 'capabilities-fields');
|
|
33
|
+
for (const [label, key] of [['什么情况下开始', 'trigger'], ['时间与频率', 'cadence'], ['什么时候保持安静', 'quiet'], ['如何停止', 'stop']]) {
|
|
34
|
+
const field = el('div'); field.append(el('dt', label), el('dd', item[key])); fields.append(field);
|
|
35
|
+
}
|
|
36
|
+
details.append(fields); section.append(details);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
panel.append(section);
|
|
40
|
+
|
|
41
|
+
const access = data?.dataAccess;
|
|
42
|
+
const datasets = el('section', undefined, 'capabilities-section');
|
|
43
|
+
datasets.append(el('h2', '平台数据权限'), el('p', '这里展示工程声明的数据集和用途。声明不代表用户已授权;搭子自行保存的业务记录不属于平台数据权限。', 'capabilities-note'));
|
|
44
|
+
if (access?.status === 'declared') {
|
|
45
|
+
if (!access.items.length) datasets.append(el('p', '未声明平台数据集', 'capabilities-empty'));
|
|
46
|
+
for (const item of access.items) {
|
|
47
|
+
const row = el('article', undefined, 'capabilities-dataset');
|
|
48
|
+
row.append(el('h3', item.id), el('p', item.purpose)); datasets.append(row);
|
|
49
|
+
}
|
|
50
|
+
} else datasets.append(el('p', access?.message || '正在读取数据权限声明。', 'capabilities-empty'));
|
|
51
|
+
panel.append(datasets);
|
|
52
|
+
}
|
|
@@ -3,5 +3,6 @@
|
|
|
3
3
|
<link rel="stylesheet" href="./reader.css?v=1">
|
|
4
4
|
<script type="module" src="./reader.js?v=1"></script>
|
|
5
5
|
<link rel="stylesheet" href="./widgets.css?v=1">
|
|
6
|
-
<script type="module" src="./widgets.js?v=
|
|
6
|
+
<script type="module" src="./widgets.js?v=2"></script>
|
|
7
|
+
<link rel="stylesheet" href="./capabilities.css?v=1">
|
|
7
8
|
</head><body><div id="root"></div></body></html>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Read-only mirror of the host's saved design workflow. No confirmation writes.
|
|
2
|
+
import { renderCapabilities } from './capabilities.js';
|
|
2
3
|
const el = (tag, text, cls) => {
|
|
3
4
|
const node = document.createElement(tag);
|
|
4
5
|
if (text !== undefined) node.textContent = text;
|
|
@@ -10,25 +11,33 @@ const nav = el('nav', undefined, 'design-navigation');
|
|
|
10
11
|
nav.setAttribute('aria-label', '创作内容');
|
|
11
12
|
const bookButton = el('button', '四册 Booklet');
|
|
12
13
|
const designButton = el('button', '小挂件设计');
|
|
14
|
+
const capabilitiesButton = el('button', '主动性与数据');
|
|
13
15
|
const progress = el('span', '正在读取创作进度', 'design-progress');
|
|
14
|
-
nav.append(bookButton, designButton, progress);
|
|
16
|
+
nav.append(bookButton, designButton, capabilitiesButton, progress);
|
|
15
17
|
document.body.prepend(nav);
|
|
16
18
|
const panel = el('main', undefined, 'design-workspace');
|
|
17
19
|
panel.hidden = true;
|
|
18
20
|
document.body.append(panel);
|
|
19
|
-
|
|
21
|
+
const capabilitiesPanel = el('main', undefined, 'capabilities-workspace');
|
|
22
|
+
document.body.append(capabilitiesPanel);
|
|
23
|
+
let chosen = false, previousPhase, version, capabilitiesVersion, activeView;
|
|
20
24
|
const selectedStates = new Map();
|
|
21
|
-
function choose(
|
|
25
|
+
function choose(view, manual = false) {
|
|
26
|
+
activeView = view;
|
|
22
27
|
if (manual) chosen = true;
|
|
23
|
-
root.hidden =
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
root.hidden = view !== 'booklets'; panel.hidden = view !== 'widgets';
|
|
29
|
+
capabilitiesPanel.hidden = view !== 'capabilities';
|
|
30
|
+
bookButton.setAttribute('aria-pressed', String(view === 'booklets'));
|
|
31
|
+
designButton.setAttribute('aria-pressed', String(view === 'widgets'));
|
|
32
|
+
capabilitiesButton.setAttribute('aria-pressed', String(view === 'capabilities'));
|
|
33
|
+
progress.hidden = view === 'capabilities';
|
|
26
34
|
// Do not stop the upstream reader before its first snapshot selects a stage.
|
|
27
35
|
if (manual && root.querySelector('.preview-header')) document.dispatchEvent(new Event('buddy:reader-browse'));
|
|
28
36
|
}
|
|
29
|
-
bookButton.onclick = () => choose(
|
|
30
|
-
designButton.onclick = () => choose(
|
|
31
|
-
choose(
|
|
37
|
+
bookButton.onclick = () => choose('booklets', true);
|
|
38
|
+
designButton.onclick = () => choose('widgets', true);
|
|
39
|
+
capabilitiesButton.onclick = () => choose('capabilities', true);
|
|
40
|
+
choose('booklets');
|
|
32
41
|
const card = title => { const node = el('section', undefined, 'design-card'); node.append(el('h2', title)); return node; };
|
|
33
42
|
const badge = (text, pass = false) => el('span', text, 'design-badge' + (pass ? ' passed' : ''));
|
|
34
43
|
function field(parent, label, value) { const row = el('div', undefined, 'design-field'); row.append(el('dt', label), el('dd', value)); parent.append(row); }
|
|
@@ -39,7 +48,7 @@ function render(snapshot) {
|
|
|
39
48
|
nav.hidden = false;
|
|
40
49
|
const index = design.steps.findIndex(step => step.id === design.phase);
|
|
41
50
|
progress.textContent = design.enabled ? design.steps.map((step, i) => (i < index ? '✓ ' : '') + step.label).join(' → ') : '已有作品';
|
|
42
|
-
if (!chosen && design.phase !== 'booklets' && design.phase !== 'legacy' && previousPhase !== design.phase) choose(
|
|
51
|
+
if (!chosen && design.phase !== 'booklets' && design.phase !== 'legacy' && previousPhase !== design.phase) choose('widgets');
|
|
43
52
|
previousPhase = design.phase;
|
|
44
53
|
panel.replaceChildren();
|
|
45
54
|
const heading = el('div', undefined, 'design-heading');
|
|
@@ -155,14 +164,20 @@ async function update() {
|
|
|
155
164
|
const response = await fetch('./api/snapshot');
|
|
156
165
|
if (!response.ok) throw new Error('snapshot unavailable');
|
|
157
166
|
const snapshot = await response.json();
|
|
167
|
+
const nextCapabilities = JSON.stringify(snapshot.capabilities);
|
|
168
|
+
if (nextCapabilities !== capabilitiesVersion) {
|
|
169
|
+
capabilitiesVersion = nextCapabilities;
|
|
170
|
+
renderCapabilities(capabilitiesPanel, snapshot.capabilities);
|
|
171
|
+
}
|
|
172
|
+
progress.hidden = activeView === 'capabilities';
|
|
158
173
|
const next = JSON.stringify([snapshot.widgetDesign, snapshot.completion, snapshot.paused]);
|
|
159
174
|
if (next !== version) { version = next; render(snapshot); }
|
|
160
|
-
} catch { progress.textContent = '页面暂时无法同步;已展示的内容保留,请回主对话查看进度。'; }
|
|
175
|
+
} catch { version = undefined; progress.hidden = false; progress.textContent = '页面暂时无法同步;已展示的内容保留,请回主对话查看进度。'; }
|
|
161
176
|
finally { loading = false; }
|
|
162
177
|
}
|
|
163
178
|
update();
|
|
164
179
|
const events = new EventSource('./api/events');
|
|
165
180
|
events.addEventListener('changed', update);
|
|
166
181
|
events.addEventListener('ready', update);
|
|
167
|
-
events.addEventListener('unavailable', () => { progress.textContent = '内容暂时无法读取,请回主对话检查。'; });
|
|
182
|
+
events.addEventListener('unavailable', () => { version = undefined; progress.hidden = false; progress.textContent = '内容暂时无法读取,请回主对话检查。'; });
|
|
168
183
|
document.addEventListener('visibilitychange', () => { if (!document.hidden) update(); });
|
package/package.json
CHANGED
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
|
|
34
34
|
## service.blueprint 的 data
|
|
35
35
|
|
|
36
|
+
可选的 `proactiveReminders` 数组用于 Booklet「主动性与数据」页,结构和填写时机见 [主动性与数据](capabilities.md)。它随蓝图一起保存和确认;旧作品缺少此字段仍可原样恢复。
|
|
37
|
+
|
|
36
38
|
可以先保存带未决项的草案;以下字段与业务判断齐备后才能正式确认。字段是内部记录格式,不把这张表变成一次询问用户的清单。智能规划基于已有信息给完整建议,只有实际缺口才追问。
|
|
37
39
|
|
|
38
40
|
| 字段 | 形状与含义 |
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# 主动性与数据
|
|
2
|
+
|
|
3
|
+
Booklet 的「主动性与数据」与四册、小挂件设计并列,集中展示提醒约定和平台数据权限。页面只读,不增加第五册、单独审批或新的创作阶段。
|
|
4
|
+
|
|
5
|
+
## 主动提醒来自服务方案
|
|
6
|
+
|
|
7
|
+
整理服务蓝图时,复用已有任务、交付节奏和创作者意见,在 `service.blueprint.data.proactiveReminders` 记录提醒规则。没有提到主动服务,不等于已决定不做;实际缺口沿用服务阶段的讨论,不为了填页面另开问卷。已有作品缺少该字段时显示“尚未整理”,不自动补写或使既有交付失效。
|
|
8
|
+
|
|
9
|
+
数组每项包含以下非空文本,最多 32 项;说明字段每项最多 2000 字:
|
|
10
|
+
|
|
11
|
+
| 字段 | 内容 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| id | 稳定且不重复的小写英文 ID,可含数字、连字符,最长 64 位 |
|
|
14
|
+
| name | 提醒名称,例如“晚餐备菜提醒” |
|
|
15
|
+
| purpose | 帮用户完成什么 |
|
|
16
|
+
| trigger | 什么情况下开始,包括是否需要用户提出或开启 |
|
|
17
|
+
| cadence | 时间、时区、频率,以及单次还是持续提醒 |
|
|
18
|
+
| quiet | 什么情况下不提醒,如何避免重复打扰 |
|
|
19
|
+
| stop | 用户如何停止、什么情况下自动结束、是否可能再次开启 |
|
|
20
|
+
|
|
21
|
+
按真实约定填写,不将示例复制为默认能力。尚未明确的条件在字段里如实写“待明确”,并保留蓝图的 unresolved。明确不需要主动提醒时用空数组;它随整个服务蓝图展示、修订与确认,不单独再确认一次。方案更新正常走产物协议,不能直接改 state.json 或借旧确认确认新内容。
|
|
22
|
+
|
|
23
|
+
提醒随服务蓝图导出到 `service-blueprint.json`;在服务手册正文中也讲清对应约定。页面展示的是方案,不能宣称调度已实现或用户已开启。开发时须核对实际的触发、静默、取消与重新启用行为,有差异先说明并按用户决定修订方案或实现。
|
|
24
|
+
|
|
25
|
+
## 平台数据权限来自实际工程
|
|
26
|
+
|
|
27
|
+
CLI 工程的面板利用 `.buddy/creator/session.json`、作品 creation-key 与 `package.json.buddyId` 核对工程归属,只读取该工程根目录的 `data-access.json.requirements`。展示原始数据集 ID 和用途,不另外维护一份权限清单或推测授权状态。读取不依赖 Node,不运行工程代码;ID 是否受支持由 CLI / Runtime 预检校验。
|
|
28
|
+
|
|
29
|
+
- 有效的空数组显示“未声明平台数据集”。
|
|
30
|
+
- 文件缺失、声明损坏或工程不匹配分别显示未提供或无法读取,不能解释成“不需要权限”。独立 Creator 作品未关联 CLI 工程时显示未关联。
|
|
31
|
+
- 用户自己的菜谱、喂养记录等业务数据不混入平台数据权限;平台数据权限只来自 requirements。
|
|
32
|
+
- 实际开发时通过 `buddy-cli datasets` 查询目录,将需要的 ID 和用途落到 `data-access.json`,更新文件后页面自动同步。声明不代表终端用户授权,也不证明 Runtime 已登记成功。
|
|
33
|
+
|
|
34
|
+
页面不修改任何配置、定时任务或授权;没有确认按钮,不显示内部来源路径、实现函数或核对日期。
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# CLI 创作接续协议
|
|
2
2
|
|
|
3
|
+
Booklet 的「主动性与数据」展示服务蓝图的主动提醒规则,以及当前 CLI 工程 `data-access.json` 中的数据集 ID 和用途。交还开发时同时读取导出的 `service-blueprint.json`,实现触发、静默和停止约定;实际数据声明由 Coding Agent 按 CLI 指引维护,不能把创作方案当作已实现或已授权。详细约定见 [主动性与数据](capabilities.md)。
|
|
4
|
+
|
|
3
5
|
本文件由 Skill 维护。CLI 交接提供项目身份、初始背景文件、workspace、creation-key 与 `open` 参数;平台 buddyId 不替换本地作品 ID,也不重新登记云端搭子。
|
|
4
6
|
|
|
5
7
|
完整读取 SKILL.md 与 host-guide.md,由当前主对话完成采访。使用 Python 3.9+,所有调用加 -B,并设置 PYTHONDONTWRITEBYTECODE=1;程序只读 Skill 目录,作品始终写入 CLI 指定的 workspace。无需安装 pip/npm 依赖或启动独立采访模型服务。
|
package/scripts/buddy_core.py
CHANGED
|
@@ -395,6 +395,9 @@ def put_artifacts(state, proposals, by, draft=False):
|
|
|
395
395
|
elif a['kind'] == 'blueprint':
|
|
396
396
|
require(a['id'] == 'service.blueprint' and a['stage'] == 'service', 'BLUEPRINT_SCHEMA', '服务蓝图 ID 必须为 service.blueprint。')
|
|
397
397
|
data = a['data']
|
|
398
|
+
if 'proactiveReminders' in data:
|
|
399
|
+
import capabilities
|
|
400
|
+
capabilities.validate_reminders(data['proactiveReminders'])
|
|
398
401
|
require(data.get('platformRules') == catalog()['serviceRules'], 'FIXED_SERVICE_RULES', 'platformRules 必须复制 context.catalog.serviceRules 当前规则。')
|
|
399
402
|
require(data.get('conversationLimit', 'unlimited') == 'unlimited' and data.get('maintenanceContract', {}).get('basicConversation', True) is True, 'SERVICE_CONVERSATION_POLICY', '付费期间 AI 对话不限次数,维持期间保留基础对话。')
|
|
400
403
|
period = data.get('billingCycle')
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Read-only projection of authored reminders and the CLI project's declarations."""
|
|
2
|
+
import copy
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import buddy_core as core
|
|
8
|
+
|
|
9
|
+
REMINDER_FIELDS = ('id', 'name', 'purpose', 'trigger', 'cadence', 'quiet', 'stop')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_reminders(value):
|
|
13
|
+
core.require(isinstance(value, list) and len(value) <= 32, 'REMINDERS_SCHEMA', 'proactiveReminders 必须是最多 32 项的数组。')
|
|
14
|
+
seen = set()
|
|
15
|
+
for item in value:
|
|
16
|
+
core.require(isinstance(item, dict) and set(item) == set(REMINDER_FIELDS)
|
|
17
|
+
and all(isinstance(item[k], str) and item[k].strip() and len(item[k]) <= 2000 for k in REMINDER_FIELDS),
|
|
18
|
+
'REMINDERS_SCHEMA', '每项提醒需完整说明名称、用途、触发、频率、静默和停止条件。')
|
|
19
|
+
core.require(re.fullmatch(r'[a-z][a-z0-9-]{0,63}', item['id']) and item['id'] not in seen,
|
|
20
|
+
'REMINDERS_SCHEMA', '提醒 ID 使用不重复的小写英文、数字或连字符。')
|
|
21
|
+
seen.add(item['id'])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read(path):
|
|
25
|
+
# Never follow a declaration symlink outside the bound project.
|
|
26
|
+
if path.is_symlink() or not path.is_file() or path.stat().st_size > 1024 * 1024:
|
|
27
|
+
raise ValueError('invalid local declaration')
|
|
28
|
+
return json.loads(path.read_text(encoding='utf-8'))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _project(workspace, state):
|
|
32
|
+
# CLI already records this binding. Do not guess an arbitrary ancestor's project.
|
|
33
|
+
workspace = Path(workspace).resolve()
|
|
34
|
+
if [p.name for p in list(workspace.parents)[:3]] != ['workspaces', 'creator', '.buddy']:
|
|
35
|
+
return None
|
|
36
|
+
creator = workspace.parent.parent
|
|
37
|
+
project = creator.parent.parent
|
|
38
|
+
session = _read(creator / 'session.json')
|
|
39
|
+
package = _read(project / 'package.json')
|
|
40
|
+
if (not isinstance(session, dict) or not isinstance(package, dict)
|
|
41
|
+
or session.get('schemaVersion') != 1 or not isinstance(session.get('creationKey'), str)
|
|
42
|
+
or not isinstance(session.get('platformBuddyId'), str) or not session['platformBuddyId']
|
|
43
|
+
or core.digest(session['creationKey']) != state.get('creationKeyHash')
|
|
44
|
+
or session['platformBuddyId'] != package.get('buddyId')):
|
|
45
|
+
raise ValueError('project binding mismatch')
|
|
46
|
+
return project
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def data_access(workspace, state):
|
|
50
|
+
try:
|
|
51
|
+
project = _project(workspace, state)
|
|
52
|
+
if project is None:
|
|
53
|
+
return {'status': 'unlinked', 'message': '尚未关联开发工程,暂时无法读取平台数据权限。', 'items': []}
|
|
54
|
+
path = project / 'data-access.json'
|
|
55
|
+
if not path.exists() and not path.is_symlink():
|
|
56
|
+
return {'status': 'missing', 'message': '工程尚未提供数据权限声明,暂时无法确认需要哪些数据。', 'items': []}
|
|
57
|
+
manifest = _read(path)
|
|
58
|
+
if (not isinstance(manifest, dict) or set(manifest) != {'requirements'}
|
|
59
|
+
or not isinstance(manifest['requirements'], list) or len(manifest['requirements']) > 32):
|
|
60
|
+
raise ValueError('invalid requirements')
|
|
61
|
+
items, seen = [], set()
|
|
62
|
+
for item in manifest['requirements']:
|
|
63
|
+
if (not isinstance(item, dict) or set(item) != {'id', 'purpose'}
|
|
64
|
+
or not isinstance(item['id'], str) or not isinstance(item['purpose'], str)):
|
|
65
|
+
raise ValueError('invalid requirement')
|
|
66
|
+
identifier, purpose = item['id'].strip(), item['purpose'].strip()
|
|
67
|
+
if (not re.fullmatch(r'[a-zA-Z0-9_.-]{1,200}', identifier)
|
|
68
|
+
or not 1 <= len(purpose) <= 200 or identifier in seen):
|
|
69
|
+
raise ValueError('invalid requirement')
|
|
70
|
+
seen.add(identifier)
|
|
71
|
+
items.append({'id': identifier, 'purpose': purpose})
|
|
72
|
+
return {'status': 'declared', 'items': items}
|
|
73
|
+
except (OSError, ValueError, TypeError):
|
|
74
|
+
return {'status': 'error', 'message': '数据权限声明或工程关联无法读取,请在主对话中检查。', 'items': []}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def projection(workspace, state):
|
|
78
|
+
blueprint = state.get('artifacts', {}).get('service.blueprint', {})
|
|
79
|
+
reminders = blueprint.get('data', {}).get('proactiveReminders')
|
|
80
|
+
view = {'status': 'missing', 'items': [], 'message': '尚未整理主动提醒规则。'}
|
|
81
|
+
if 'proactiveReminders' in blueprint.get('data', {}):
|
|
82
|
+
try:
|
|
83
|
+
validate_reminders(reminders)
|
|
84
|
+
view = {'status': 'confirmed' if core.confirmed(state, 'service.blueprint') else 'draft',
|
|
85
|
+
'items': copy.deepcopy(reminders)}
|
|
86
|
+
except core.BuddyError:
|
|
87
|
+
view = {'status': 'error', 'items': [], 'message': '主动提醒规则无法读取,请在主对话中检查。'}
|
|
88
|
+
return {'reminders': view, 'dataAccess': data_access(workspace, state)}
|
package/scripts/preview.py
CHANGED
|
@@ -319,12 +319,14 @@ def snapshot(workspace, state=None):
|
|
|
319
319
|
if complete:
|
|
320
320
|
completion_view = {"revision": state["revision"], "status": "failed", "message": "四册已确认,成果暂时无法读取。"}
|
|
321
321
|
import widgets
|
|
322
|
+
import capabilities
|
|
322
323
|
widget_design = widgets.projection(state, workspace)
|
|
323
324
|
if complete and widgets.enabled(state) and not widgets.ready(state):
|
|
324
325
|
current.update(phase='小挂件交互设计', title=widget_design['nextStep'], status='待确认')
|
|
325
326
|
return {"buddyId": state["buddyId"], "revision": state["revision"], "stage": stage,
|
|
326
327
|
"paused": bool(state.get("paused")), "activity": _activity(state, drafts),
|
|
327
|
-
"completion": completion_view, "widgetDesign": widget_design,
|
|
328
|
+
"completion": completion_view, "widgetDesign": widget_design,
|
|
329
|
+
"capabilities": capabilities.projection(workspace, state), "current": current, "stages": stages,
|
|
328
330
|
"artifacts": artifacts, "sources": sources, "drafts": visible_drafts}
|
|
329
331
|
|
|
330
332
|
|