release: prepare 0629001 portal updates

This commit is contained in:
john
2026-06-29 22:20:04 +08:00
parent 18ea4f82fd
commit a40e340a41
84 changed files with 17516 additions and 2475 deletions
+3 -1
View File
@@ -13,10 +13,12 @@ Required persisted items to inherit on the host:
Bundled alongside server.mjs (required for sandbox-fs MCP):
mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)
Platform agent skills (synced into user workspaces):
skills/
Key runtime differences must stay in .env, not in the artifact:
DATABASE_URL / MYSQL_*
H5_PUBLIC_BASE_URL
TKMIND_API_TARGET / TKMIND_API_TARGET_1
TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1
H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT
Deployment and operations transport:
@@ -884,6 +884,9 @@ function resolveSandboxed(p) {
if (resolved !== SANDBOX && !resolved.startsWith(SANDBOX + path.sep)) {
throw Object.assign(new Error(`\u8DEF\u5F84\u8D8A\u754C\uFF1A${p} \u4E0D\u5728\u5F53\u524D\u5DE5\u4F5C\u533A\u5185`), { code: "EACCES" });
}
if (resolved === PRIVATE_DATA_DIR || resolved.startsWith(PRIVATE_DATA_DIR + path.sep)) {
throw Object.assign(new Error("\u7981\u6B62\u76F4\u63A5\u8BBF\u95EE\u79C1\u6709\u6570\u636E\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528 private_data_* \u5DE5\u5177"), { code: "EACCES" });
}
return resolved;
}
var ALL_TOOLS = [
+1
View File
@@ -373,6 +373,7 @@ CREATE TABLE IF NOT EXISTS h5_usage_records (
cost_cents BIGINT NOT NULL,
balance_after_cents BIGINT NOT NULL,
created_at BIGINT NOT NULL,
UNIQUE KEY uniq_h5_usage_request_id (request_id),
KEY idx_h5_usage_user_time (user_id, created_at),
CONSTRAINT fk_h5_usage_user FOREIGN KEY (user_id) REFERENCES h5_users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -14,8 +14,11 @@ fi
export NODE_ENV=production
export H5_PORT="${H5_PORT:-8081}"
export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"
export TKMIND_API_TARGETS="${TKMIND_API_TARGETS:-https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009}"
export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"
export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"
export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-/usr/local/bin/node}"
export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-/opt/portal/mindspace-sandbox-mcp.mjs}"
NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"
if [[ ! -x "${NODE_BIN}" ]]; then
+11230 -1455
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
---
name: code-playground
description: 代码展示与运行技能:在聊天中展示代码并支持 JavaScript 沙盒执行(需启用 codeplayground 扩展)
---
# 代码演练场
使用 `codeplayground` MCP 扩展在聊天中展示可运行的代码片段。
## 何时使用
- 生成代码示例后,让用户直接在聊天里运行验证
- 展示算法、数据处理逻辑
- JavaScript 演示(可执行);其他语言(展示+复制)
## 前提
Settings → Extensions 中启用 **Code Playground** 扩展。
## 工具
`show_playground(title, code, language, description?)` — 展示代码并可选执行
## 支持语言
- **JavaScript** — 有「运行」按钮,`console.log` 输出显示在下方
- 其他语言(`rust``python``typescript` 等)— 展示+语法高亮+复制按钮
## 规则
1. JS 执行在沙盒 iframe 中,无网络访问,无 DOM 操作
2. 代码不超过 200 行,超出拆分展示
3. 有副作用(写文件、网络请求)的代码标注「仅供参考,不可直接运行」
@@ -0,0 +1,28 @@
---
name: diff-viewer
description: 代码对比可视化技能:以高亮 diff 形式展示文件修改前后(需启用 diffviewer 扩展)
---
# Diff 查看器
使用 `diffviewer` MCP 扩展在聊天中渲染代码修改对比。
## 何时使用
- 用户要看代码改动前后对比
- 展示重构、bug 修复、配置变更的效果
- 比 markdown 代码块更直观地呈现变更
## 前提
Settings → Extensions 中启用 **Diff Viewer** 扩展。
## 工具
`show_diff(title, old_content, new_content, language?)` — 渲染带行号的 diff
## 规则
1. `language` 填文件类型(`rust``typescript``python` 等),用于语法提示
2. 内容太大时只截取关键改动部分,加说明
3. 配合 `git_diff` 工具使用效果最佳
@@ -0,0 +1,80 @@
---
name: docx-generate
description: 在工作区内用 Python 标准库(zipfile + XML)生成 Word .docx,无需 python-docx 或 docx_tool
---
# Word 文档生成(.docx
## 重要说明
- **平台没有 `docx_tool` / `update_doc`**,不要编造或调用不存在的工具
- **`write_file` 不能写二进制 .docx**,不要用 write_file 假装生成 Word
- **禁止**在 HTML 里用 `data:application/...;base64,...` 内嵌 docx(极易被截断损坏,下载后乱码)
- 需要公网下载时:先用本脚本生成 `.docx` 落盘,再在 HTML 里用**相对路径**链接(如 `public/方案.docx`
- 生产沙箱通常**不能 pip install**,优先用本技能自带的 **stdlib 脚本**
- 生成后必须用 **`list_dir oa/`**(或目标目录)确认文件已落盘,再告诉用户
## 何时使用
- 用户要 Word / docx / .doc 文档(输出 `.docx`
- 需要保存到 `oa/``private/` 等分区
## 推荐命令
技能目录内有 `generate_docx.py`(仅依赖 Python 3 标准库):
```bash
python3 .agents/skills/docx-generate/generate_docx.py --json - --output oa/报告.docx <<'EOF'
{
"title": "文档标题",
"sections": [
{
"heading": "一、章节标题",
"paragraphs": ["段落一", "段落二"],
"table": {
"headers": ["列1", "列2"],
"rows": [["A", "B"], ["C", "D"]]
}
}
]
}
EOF
```
然后:
```bash
list_dir oa
```
## JSON 字段
| 字段 | 说明 |
|------|------|
| `title` | 文档主标题(可选) |
| `sections[]` | 章节数组 |
| `sections[].heading` | 章节标题 |
| `sections[].paragraphs` | 字符串段落列表 |
| `sections[].table.headers` | 表头 |
| `sections[].table.rows` | 表格行 |
**禁止**把 `<w:tbl>` 等 OOXML 标签写进 `paragraphs` 文本里;表格只能走 `table` 字段。
## 公网下载页(HTML + docx
用户要「打开链接下载 Word」时:
1. 用本脚本生成 docx(建议 `public/文件名.docx``oa/文件名.docx` 再复制到 `public/`
2.`static-page-publish` 写下载页,`href` 指向**同目录相对路径**
```html
<a href="medical-plan.docx" download>下载 Word 文档</a>
```
3. **禁止** `href="data:...;base64,..."` 嵌入 docx
## 备选方案
1. **用户要在线查看、可分享**:用 `static-page-publish` 技能写 `public/xxx.html`
2. **环境有 python-docx**(工作区 `.venv` 已装):仍可用,但生成后必须 `list_dir` 验证
3. **禁止**在未验证文件存在时宣称「已生成 docx」
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Generate a minimal valid .docx using only Python stdlib (zipfile + XML)."""
from __future__ import annotations
import argparse
import json
import sys
import zipfile
from pathlib import Path
from xml.sax.saxutils import escape
W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
CT_NS = "http://schemas.openxmlformats.org/package/2006/content-types"
REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships"
CP_NS = "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
DC_NS = "http://purl.org/dc/elements/1.1"
CONTENT_TYPES = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="{CT_NS}">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
</Types>"""
ROOT_RELS = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="{REL_NS}">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
</Relationships>"""
DOCUMENT_RELS = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="{REL_NS}"/>"""
def sanitize_text(text: str) -> str:
"""Strip emoji and XML-illegal control chars (Word/iOS rejects these)."""
cleaned = str(text or "")
cleaned = cleaned.encode("utf-8", "ignore").decode("utf-8")
cleaned = "".join(
ch
for ch in cleaned
if not (0xD800 <= ord(ch) <= 0xDFFF)
and (ord(ch) == 0x9 or ord(ch) == 0xA or ord(ch) == 0xD or ord(ch) >= 0x20)
and not (0x1F000 <= ord(ch) <= 0x1FAFF or 0x2600 <= ord(ch) <= 0x27BF)
)
return cleaned.strip()
def xml_text(text: str) -> str:
return escape(sanitize_text(text))
def run(text: str, bold: bool = False) -> str:
props = "<w:b/>" if bold else ""
return (
f'<w:r><w:rPr>{props}</w:rPr>'
f'<w:t xml:space="preserve">{xml_text(text)}</w:t></w:r>'
)
def paragraph(parts: str, style: str | None = None) -> str:
ppr = f'<w:pPr><w:pStyle w:val="{style}"/></w:pPr>' if style else ""
return f"<w:p>{ppr}{parts}</w:p>"
def heading(text: str) -> str:
cleaned = sanitize_text(text)
if not cleaned:
return ""
return paragraph(run(cleaned, bold=True))
def body_paragraph(text: str) -> str:
cleaned = sanitize_text(text)
if not cleaned:
return ""
return paragraph(run(cleaned))
def is_ooxml_fragment(text: str) -> bool:
stripped = str(text or "").strip()
return stripped.startswith("<w:") or "</w:" in stripped
def table_block(headers: list[str], rows: list[list[str]]) -> str:
col_count = max(len(headers), max((len(r) for r in rows), default=0))
if col_count == 0:
return ""
col_width = max(1800, 9000 // col_count)
def cell(text: str) -> str:
return (
f'<w:tc><w:tcPr><w:tcW w:w="{col_width}" w:type="dxa"/></w:tcPr>'
f"{paragraph(run(text))}</w:tc>"
)
def table_row(cells: list[str]) -> str:
padded = cells + [""] * (col_count - len(cells))
return f"<w:tr>{''.join(cell(c) for c in padded[:col_count])}</w:tr>"
grid_cols = "".join(f'<w:gridCol w:w="{col_width}"/>' for _ in range(col_count))
tbl_pr = (
"<w:tblPr>"
'<w:tblW w:w="5000" w:type="pct"/>'
"<w:tblBorders>"
'<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
'<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
'<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
'<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
'<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
'<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>'
"</w:tblBorders>"
"</w:tblPr>"
)
# Table must be a direct child of w:body — never wrap w:tbl inside w:p.
parts = ["<w:tbl>", tbl_pr, f"<w:tblGrid>{grid_cols}</w:tblGrid>"]
if headers:
parts.append(table_row(headers))
parts.extend(table_row(r) for r in rows)
parts.append("</w:tbl>")
return "".join(parts)
def build_document_xml(title: str, sections: list[dict]) -> str:
body: list[str] = []
if title:
block = heading(title)
if block:
body.append(block)
for section in sections:
heading_text = section.get("heading") or section.get("title")
if heading_text:
block = heading(str(heading_text))
if block:
body.append(block)
for para in section.get("paragraphs") or []:
text = str(para).strip()
if text and not is_ooxml_fragment(text):
block = body_paragraph(text)
if block:
body.append(block)
table = section.get("table")
if isinstance(table, dict):
block = table_block(
list(table.get("headers") or []),
[list(r) for r in (table.get("rows") or [])],
)
if block:
body.append(block)
sect_pr = (
f'<w:sectPr><w:pgSz w:w="11906" w:h="16838"/>'
f'<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr>'
)
inner = "".join(body) + sect_pr
return (
f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
f'<w:document xmlns:w="{W_NS}" xmlns:r="{R_NS}">'
f"<w:body>{inner}</w:body></w:document>"
)
def core_xml(title: str) -> str:
return (
f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
f'<cp:coreProperties xmlns:cp="{CP_NS}" xmlns:dc="{DC_NS}">'
f"<dc:title>{xml_text(title)}</dc:title>"
f"</cp:coreProperties>"
)
def write_docx(output: Path, title: str, sections: list[dict]) -> None:
output.parent.mkdir(parents=True, exist_ok=True)
document_xml = build_document_xml(title, sections)
with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("[Content_Types].xml", CONTENT_TYPES)
zf.writestr("_rels/.rels", ROOT_RELS)
zf.writestr("word/_rels/document.xml.rels", DOCUMENT_RELS)
zf.writestr("word/document.xml", document_xml)
zf.writestr("docProps/core.xml", core_xml(title))
def load_payload(args: argparse.Namespace) -> dict:
if args.json:
source = sys.stdin.read() if args.json == "-" else Path(args.json).read_text(encoding="utf-8")
return json.loads(source)
if args.title:
return {"title": args.title, "sections": [{"paragraphs": args.paragraph or []}]}
raise SystemExit("需要 --json <file>(或 - 读 stdin),或 --title")
def main() -> None:
parser = argparse.ArgumentParser(description="用 Python 标准库生成 .docx(无需 python-docx")
parser.add_argument("--output", required=True, help="输出路径,如 oa/报告.docx")
parser.add_argument("--json", help="JSON 内容文件;传 - 则从 stdin 读取")
parser.add_argument("--title", help="仅标题 + --paragraph 时的文档标题")
parser.add_argument("--paragraph", action="append", help="配合 --title 追加段落")
args = parser.parse_args()
payload = load_payload(args)
title = str(payload.get("title") or "")
sections = list(payload.get("sections") or [])
output = Path(args.output)
write_docx(output, title, sections)
size = output.stat().st_size
print(f"已生成 {output}{size} 字节)")
if __name__ == "__main__":
main()
@@ -0,0 +1,38 @@
---
name: form-builder
description: 动态表单技能:向用户展示结构化输入表单,收集多字段信息(需启用 formbuilder 扩展)
---
# 表单收集
使用 `formbuilder` MCP 扩展在聊天中展示交互式表单,替代多轮逐一询问。
## 何时使用
- 需要收集 3 个以上结构化字段
- 有下拉选项、必填校验的场景
- 配置向导、信息录入、参数填写
## 前提
Settings → Extensions 中启用 **Form Builder** 扩展。
## 工具
`show_form(title, description?, fields)` — 渲染表单并等待用户提交
## 字段类型
| type | 说明 |
|------|------|
| `text` | 单行文本 |
| `number` | 数字 |
| `textarea` | 多行文本 |
| `select` | 下拉选择(需提供 `options` |
| `checkbox` | 勾选框 |
## 规则
1. 用户提交后,结果以 JSON 回传,解析后继续任务
2. `required: true` 的字段前端会校验,不会提交空值
3. 表单不超过 8 个字段,太多拆成多步
+30
View File
@@ -0,0 +1,30 @@
---
name: git
description: Git 操作技能:查看状态、diff、提交历史、blame、提交代码
---
# Git 操作
使用内置 `git` 平台扩展完成常见版本控制任务。
## 何时使用
- 用户询问代码改动、提交历史、谁修改了某行
- 用户要提交代码、查看 diff、切换分支
## 工具
| 工具 | 用途 |
|------|------|
| `git_status` | 查看工作区状态和当前分支 |
| `git_diff` | 查看未暂存或已暂存的改动 |
| `git_log` | 查看提交历史 |
| `git_blame` | 查看指定文件某几行的最后修改人 |
| `git_commit` | 暂存指定文件并提交(自动添加 -s 签名) |
## 规则
1. `git_commit` 前先用 `git_status` 确认改动范围,避免误提交
2. 提交信息用英文、简短、说明 why 而非 what
3. 不要提交 `.env`、密钥、大二进制文件
4. `git_diff``staged: true` 查看已暂存内容,默认查看未暂存
+43
View File
@@ -0,0 +1,43 @@
---
name: kanban
description: 看板可视化技能:将任务列表渲染为多列看板(需启用 kanban 扩展)
---
# 看板展示
使用 `kanban` MCP 扩展在聊天中渲染任务看板。
## 何时使用
- 用户要查看项目进度、任务分布
- 展示待办/进行中/已完成三阶段状态
- 多个任务需要按优先级可视化
## 前提
Settings → Extensions 中启用 **Kanban Board** 扩展。
## 工具
`show_kanban(title, columns)` — 渲染 N 列看板
## 数据结构
```
columns: [
{ name: "待处理", cards: [
{ id: "1", title: "修复登录 bug", priority: "high", description: "..." }
]},
{ name: "进行中", cards: [...] },
{ name: "已完成", cards: [...] }
]
```
## 优先级
`high`(红)/ `medium`(黄)/ `low`(灰)
## 规则
1. 列数不限,但超过 5 列会很挤,建议合并
2. 每列卡片不超过 20 张,否则截断并说明总数
@@ -0,0 +1,72 @@
---
name: product-campaign-page
description: 商品宣传 / 活动页技能:基于商品链接、主题、图片素材和卖点,生成可购买跳转的 H5 商品宣传页或活动落地页
---
# 商品宣传 / 活动页
本技能用于帮助电商商家、品牌主、达人或运营人员,把一个商品链接、商品信息和图片素材重新包装成更适合传播与转化的商品宣传页、活动页或 H5 落地页。
## 何时使用
- 用户要「做商品宣传页 / 活动页 / 产品落地页 / 带购买按钮的页面」
- 用户提供商品链接,希望包装成更好看的推广页面
- 用户上传商品图、Logo、模特图、详情图,希望生成页面文案和视觉结构
- 用户要做促销、上新、种草、达人推荐、节日活动或私域转化页面
## 必问信息
如果用户没有一次性给全,优先只问这 3 个问题:
1. 商品链接是什么?购买按钮会跳转到这个链接。
2. 希望页面是什么主题或风格?例如高级感、夏日清爽、科技感、国潮、节日促销、达人种草。
3. 有没有要上传的商品图、Logo、模特图、详情图或参考图?
需要生成完整页面时,再补问:
- 商品名称
- 目标人群
- 3-5 个真实卖点
- 价格、优惠、限时活动、赠品或库存信息
- 品牌色、禁用词、平台限制或合规要求
## 工作流
1. 先整理输入
- 把商品链接作为唯一购买跳转地址。
- 从用户文本和图片中提取商品名、主题、卖点、价格与素材线索。
- 不要编造价格、销量、认证、功效、明星背书或平台承诺。
2. 定义包装方向
- 给出一句清晰的商品定位。
- 说明目标人群、核心痛点、购买理由和活动钩子。
- 如果主题不明确,给用户 2-3 个可选方向。
3. 设计页面结构
- 首屏必须直接出现商品或活动主张。
- 至少包含:Hero、卖点区、使用场景或信任背书、活动优惠、底部购买 CTA。
- 主 CTA 和底部 CTA 都必须跳转到用户提供的商品链接。
4. 生成页面或文案
- 如果用户只要策划,输出页面结构、标题、副标题、卖点文案、CTA 文案和视觉方向。
- 如果用户要 H5 页面,使用 `static-page-publish` 技能在用户专属 MindSpace 发布目录生成静态 HTML,并返回 Markdown 可点击链接。
- 生成 HTML 时必须写入与商品主题一致的 `mindspace-cover` 元数据,便于信息流生成封面。
## 页面标准
- 首屏要看到商品名、核心利益点和购买按钮。
- 页面视觉必须服务商品主题,不要套用无关模板。
- 文案要具体,避免「高端大气」「品质保证」这类空泛表达。
- 有图片时优先使用用户上传素材;没有图片时明确标注需要补充的图片位置。
- 活动信息未知时用可替换占位,不要虚构折扣、倒计时或库存。
- 不处理支付,不模仿第三方平台收银台,只跳转原始商品链接。
## 推荐页面结构
1. Hero 首屏:商品名、主标题、副标题、商品图、立即购买按钮。
2. 场景痛点:用户为什么需要它。
3. 核心卖点:3-5 个真实利益点。
4. 使用场景:生活方式、达人推荐、对比或人群场景。
5. 活动优惠:价格、优惠券、赠品、限时利益点。
6. 信任背书:评价、售后、品牌说明;没有真实信息时不编造。
7. 底部 CTA:再次引导跳转到原商品链接。
@@ -0,0 +1,48 @@
---
name: schedule-assistant
description: 处理待办、提醒、日程类消息;先澄清缺失时间,再调用受限 schedule 工具写入真实记录。
---
# 日程助手
这个 skill 只处理待办、提醒、日程、行程、闹钟类需求。目标是让回复和真实数据写入一致。
## 适用范围
- 创建待办
- 创建带时间的日程或事件
- 给已有事项创建提醒
- 查询当前用户的事项列表
## 边界
1. 只能处理日程相关问题,不接管页面生成、知识问答、代码任务等其他能力。
2. 只能操作当前用户的数据,不能猜测或引用其他用户记录。
3. 没有成功调用工具前,不能说“已经设置好了”“已经加上了”。
4. 信息不完整时先追问,不要为了显得聪明而臆造时间。
## 可用工具
- `schedule_create_item`
- `schedule_create_reminder`
- `schedule_list_items`
## 工作规则
1. 先判断用户是在创建、查询,还是补充提醒。
2. 如果缺标题、事项时间、提醒时间,先追问最小必要信息。
3. 创建事项时:
- 纯待办用 `kind: task`
- 明确约会/会议/出发等时间安排可用 `kind: event`
4. 需要提醒时:
- 先创建事项
- 再调用 `schedule_create_reminder`
- 只有两个工具都成功,才对用户确认成功
5. 如果提示里给出了 `sourceMessageId`,调用 `schedule_create_item` 时必须原样传入。
6. 查询时调用 `schedule_list_items`,只按结果回答,不要编造“已经存在”。
## 回复要求
- 简短、直接
- 成功时说明创建了什么,以及是否带提醒
- 失败时明确说没写入成功,并给出下一步
+28
View File
@@ -0,0 +1,28 @@
---
name: search
description: 代码搜索技能:在代码库中搜索文本、符号、文件名(基于 ripgrep)
---
# 代码搜索
使用内置 `search` 平台扩展在工作区内快速定位代码。
## 何时使用
- 用户要找某个函数/变量/类定义在哪里
- 用户要找包含某段文字的文件
- 用户要找符合某种命名规律的文件
## 工具
| 工具 | 用途 |
|------|------|
| `search_text` | 按文本/正则搜索,支持 glob 过滤、大小写控制 |
| `search_files` | 按文件名通配符查找文件 |
| `search_symbol` | 精确匹配完整单词(适合找函数/变量名) |
## 规则
1. 优先用 `search_symbol` 找符号,精确度更高
2. `search_text``file_glob` 可缩小范围(如 `*.rs``src/**`
3. 结果超出 `max_results` 时提示用户缩小范围
@@ -0,0 +1,85 @@
---
name: static-page-publish
description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报告与页面(TKMind H5 通用技能)
---
# 静态页面 / 报告发布
本技能为 **TKMind H5 多用户环境** 的通用发布流程。安装到用户工作区后,内容会按用户替换为专属目录与公网前缀。
## 何时使用
- 用户要「生成网页 / HTML 报告 / 可视化页面 / 分享链接」
- 用户提到「放到 MindSpace」「给个能打开的链接」
## 规则摘要
1. 只在**当前用户工作区**(会话 `working_dir`)内读写与搜索,从 `.` 开始
2. 查找 CSV/文档时只用相对路径(如 `oa/report.csv`),**禁止**去上级目录、MindSpace 根目录、其它用户目录或主机路径搜索
3. 读 CSV/列目录:用工作区内的 `shell``ls oa/``cat file.csv`)或 `tree`**禁止**用公网 URL 代替
4. **禁止**用 `shell` / `cat` / `heredoc` / `echo` / `cp` 写入 `public/*.html`HTML 必须用 `write_file` / `edit_file`(shell 在容器内执行,公网链接会 404)
5. 公网链接**仅**用于让用户浏览器打开已发布的 HTML,不能用来列目录或读数据文件
6. 静态文件保存即可访问,**无需重启**
7. 页面需提供 **Word/PDF 等二进制下载** 时:文件单独落盘(如 `public/方案.docx`),链接用相对路径;**禁止**在 HTML 内用 `data:...;base64,...` 嵌入 docx(易截断损坏)
详细约束以工作区内的 `.goosehints``.agents/skills/static-page-publish/SKILL.md` 为准。
## 推荐工作流
1. 确认需求(标题、章节、视觉风格、是否需要 hero 图)
2. `write_file` 创建 `public/页面.html`(需要调整已有页面时用 `edit_file`;需要时在同目录或 `assets/` 放主图)
3.`<head>` 写入 **mindspace-cover**(必须与页面主题一致,见下文)
4. 保存后服务端**立即**生成 `<文件名>.thumbnail.svg`Agent 交互阶段即生效)
5. 按「回复格式」返回**可点击**公网链接
## 回复格式(必须)
向用户交付页面时,**必须使用 Markdown 可点击链接**
```markdown
[马来西亚旅游攻略](https://goo.tkmind.cn/MindSpace/<用户ID>/public/malaysia-travel-guide.html)
```
要求:
- **必须**使用 `[页面标题](完整URL)`,不要只给裸 URL 或「点这里」
- 页面写入 `public/` 时,URL **必须**包含 `/public/` 路径段(与磁盘路径一致)
- 标题用页面真实主题名
- 可同时给出相对路径(如 `public/malaysia-travel-guide.html`
- 说明:保存即生效,无需重启
## 信息流预览图(必须)
每个 HTML 必须在 `<head>` 包含与**页面主题一致**的元数据。系统据此生成 **精美的 3:4 信息流封面**(工作区 `*.thumbnail.svg` +「我的空间」卡片 + 保存弹窗预览):
```html
<meta name="description" content="一句话摘要,显示在预览图副标题">
<meta name="mindspace-cover" content='{"tag":"旅行","emoji":"🇲🇾","accent":"#ff6b35","accent2":"#24243e","subtitle":"马来西亚深度游","cover":"assets/hero.jpg"}'>
```
| 字段 | 要求 |
|------|------|
| `tag` | 与主题一致:旅行 / 美食 / 报告 / **运动** / **活动** 等 |
| `accent` / `accent2` | 页面主色,与 hero/背景 CSS 一致 |
| `subtitle` | 一句话卖点;未写时用 description |
| `cover` / `image` | **必须**指向高质量主图(相对 HTML 或 `https://`);见下文 |
| `emoji` | 可选;也可写在 title 中 |
### 精美预览图(必须达标)
保存 HTML 后,系统会**立即**生成 `<文件名>.thumbnail.svg` 作为卡片封面。要产出**可在信息流中直接展示的精美封面**,必须:
1. **视觉类页面**(旅行、美食、活动、运动、品牌、产品、促销等)**必须**在 `assets/` 放置高质量 hero 主图(建议宽度 ≥1200px),并在 `cover` 字段引用(如 `assets/hero.jpg`
2. **纯文字报告**可仅用配色 + tag,但仍须保证 `accent` / `subtitle` 与页面风格一致
3. `tag``accent``accent2``subtitle` 必须与页面实际视觉一致;**禁止**省略 mindspace-cover 或填无关默认值
4. 若缺少 hero 主图,封面会退化为简陋默认图,**视为未达标**
**禁止**省略 mindspace-cover 或填写与页面无关的通用配色;促销/运动/品牌页必须写明 `tag``accent``cover`
本地对比示例:`node scripts/thumbnail-preview-demo.mjs``/thumbnail-demo/`
## 附带文件下载(Word / PDF
- 二进制文件用 `docx-generate` 脚本或平台允许的方式**单独生成**,保存到 `public/`(或 `oa/` 再复制到 `public/`
- 下载按钮示例:`<a href="report.docx" download>下载文档</a>`(与 HTML 同目录时用文件名即可)
- **禁止** `<a href="data:application/vnd...;base64,...">` 内嵌 docx/pdf
@@ -0,0 +1,37 @@
---
name: table-viewer
description: 表格可视化技能:将数据渲染为可排序、筛选的交互式表格(需启用 tableviewer 扩展)
---
# 表格查看器
使用 `tableviewer` MCP 扩展在聊天中渲染交互式数据表格。
## 何时使用
- 用户展示结构化数据(数据库查询结果、CSV、对比列表)
- 数据超过 5 行且有多列时,优先用表格而非 markdown
## 前提
Settings → Extensions 中启用 **Table Viewer** 扩展。
## 工具
`show_table(title, columns, rows)` — 渲染可排序/筛选表格
## 示例
```
show_table(
title: "销售数据",
columns: ["月份", "销售额", "环比增长"],
rows: [["1月", 12000, "+5%"], ["2月", 13500, "+12.5%"]]
)
```
## 规则
1. `columns` 与每行数据顺序必须一致
2. 数值类型保持为数字(不要转成字符串),排序才准确
3. 超过 500 行时提示用户过滤后再展示
@@ -0,0 +1,28 @@
---
name: test-runner
description: 测试运行技能:运行 cargo test 并解析通过/失败结果
---
# 测试运行
使用内置 `test_runner` 平台扩展运行项目测试并分析结果。
## 何时使用
- 用户要验证改动是否破坏了现有测试
- 用户要跑某个 crate 或特定测试用例
- 需要查看有哪些测试可运行
## 工具
| 工具 | 用途 |
|------|------|
| `run_tests` | 运行测试,可按包名和测试名过滤,支持超时 |
| `list_tests` | 列出所有可用测试(不执行) |
## 规则
1. 修复 bug 后**必须**运行相关测试确认
2.`test_filter` 只跑受影响的测试,避免全量耗时
3. 默认超时 120 秒,大型集成测试可适当调高
4. 看到 FAILED 行后,重点关注 `failures:` 区段的具体错误
+42
View File
@@ -0,0 +1,42 @@
---
name: timeline
description: 时间轴可视化技能:将事件序列渲染为交互式时间线(需启用 timeline 扩展)
---
# 时间轴展示
使用 `timeline` MCP 扩展在聊天中渲染时间线。
## 何时使用
- 展示项目里程碑、发版历史
- git log 可视化
- 事件序列、历史脉络梳理
## 前提
Settings → Extensions 中启用 **Timeline** 扩展。
## 工具
`show_timeline(title, events)` — 渲染垂直时间轴
## 数据结构
```
events: [
{
date: "2024-01-15",
title: "v1.0 发布",
description: "首个正式版本上线",
category: "发版",
color: "#5c98f9" // 可选,不填自动按 category 着色
}
]
```
## 规则
1. `date` 格式灵活(`2024-01``2024-01-15``Q1 2024` 均可)
2. 同一 `category` 自动使用同一颜色
3. 事件超过 30 个时只展示关键节点,其余省略
+28
View File
@@ -0,0 +1,28 @@
---
name: web
description: 网页抓取与搜索技能:访问网页、查阅文档、搜索互联网
---
# 网页访问
使用内置 `web` 平台扩展获取网页内容和搜索信息。
## 何时使用
- 用户要查某个 API 文档、库的用法
- 用户要搜索最新新闻、技术资料
- 需要验证某个 URL 的内容
## 工具
| 工具 | 用途 |
|------|------|
| `fetch_url` | 抓取指定 URL 的内容,可提取纯文本 |
| `web_search` | 通过 DuckDuckGo 搜索,返回标题/摘要/链接列表 |
## 规则
1. 优先用 `web_search` 探索,再用 `fetch_url` 读取具体页面
2. `extract_text: true`(默认)获取可读文本,`false` 获取原始 HTML
3. 不要访问不明来源的链接,向用户确认后再访问
4. 官方文档优先于第三方博客