@lark-apaas/openclaw-extension-miaoda-coding 0.1.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
8
+ IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
9
+ INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
10
+ EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
11
+ CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
12
+ DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
13
+ ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/dist/index.mjs ADDED
@@ -0,0 +1,7 @@
1
+ import{spawn as e}from"node:child_process";import t from"node:fs";import n,{join as r,resolve as i}from"node:path";import{appendFile as a,mkdir as o,readFile as s,writeFile as c}from"node:fs/promises";import{HttpClient as l,HttpError as u}from"@lark-apaas/http-client";function d(){return i(process.env.OPENCLAW_STATE_DIR||process.cwd())}function f(e){return r(e,`workspace`)}function p(e,t){return r(e,`workspace`,t)}function m(e){return r(e,`.agent`)}function h(){let e=process.env.app_id,t=process.env.FORCE_AUTHN_INNERAPI_DOMAIN,n=process.env.FORCE_AUTHN_ACCESS_SECRET,r=process.env.FORCE_AUTHN_ACCESS_KEY;if(!e||!t||!n||!r)throw Error(`studio-client: 缺少环境变量,需要: app_id, FORCE_AUTHN_INNERAPI_DOMAIN, FORCE_AUTHN_ACCESS_KEY, FORCE_AUTHN_ACCESS_SECRET`);return{appId:e,apiUrl:t,accessSecret:n,accessKey:r}}function g(...e){for(let t of e)if(typeof t==`string`&&t)return t}async function*_(e){let t=new TextDecoder,n=``;for await(let r of e){n+=t.decode(r,{stream:!0});let e=n.split(`
2
+
3
+ `);n=e.pop();for(let t of e){let e=t.split(`
4
+ `).find(e=>e.startsWith(`data:`));if(!e)continue;let n=e.slice(5).trim();if(!(!n||n===`[DONE]`))try{yield JSON.parse(n)}catch{}}}}var v=class{#e;#t;constructor(){this.#t=h(),this.#e=new l({baseURL:this.#t.apiUrl,timeout:9e5,platform:{enabled:!0,baseURL:this.#t.apiUrl,accessKey:this.#t.accessKey,secretKey:this.#t.accessSecret,defaultClaims:{appId:this.#t.appId}}})}async createSubApp(e={}){let t=`${this.#t.apiUrl}/api/v1/studio/innerapi/openclaw/sub_app/create`,n={};e.name&&(n.name=e.name),e.description&&(n.description=e.description);let r;try{r=await this.#e.post(t,n,{headers:{"Content-Type":`application/json`}})}catch(e){if(e instanceof u&&e.response){let t=e.response.headers.get(`x-tt-logid`)??``;throw Error(`createSubApp 请求失败 (logID: ${t}): ${e.response.status}`)}throw e}let i=r.headers?.get?.(`x-tt-logid`)??``,a=await r.json();if(a.BaseResp?.StatusCode!==0&&a.BaseResp?.StatusMessage)throw Error(`createSubApp 业务错误 (logID: ${i}): ${a.BaseResp.StatusMessage}`);if(a.error_msg&&a.status_code!==`0`)throw Error(`createSubApp 服务错误 (logID: ${i}): ${a.error_msg}`);let o=g(a.appID,a.appId,a.app_id,a.data?.appID,a.data?.appId,a.data?.app_id),s=g(a.conversationID,a.conversationId,a.conversation_id,a.data?.conversationID,a.data?.conversationId,a.data?.conversation_id);if(!o){let e=Object.keys(a).join(`,`)||`(none)`,t=a.data&&typeof a.data==`object`&&Object.keys(a.data).join(`,`)||`(none)`;throw Error(`createSubApp 返回异常 (logID: ${i}): 缺少 appID,topLevelKeys=${e} dataKeys=${t}`)}return{appID:o,conversationID:s,logId:i}}async*streamChat(e){let t=`${this.#t.apiUrl}/api/v1/studio/innerapi/openclaw/sub_app/${e.appID}/stream_chat`,n={appID:e.appID,message:e.message};e.workDir&&(n.workDir=e.workDir);let r;try{r=await this.#e.post(t,n,{headers:{"Content-Type":`application/json`,Accept:`text/event-stream`}})}catch(e){if(e instanceof u&&e.response){let t=e.response.headers.get(`x-tt-logid`)??``,n=Error(`streamChat 请求失败 (logID: ${t}): ${e.response.status}`);throw n.logId=t,n}let t=e.cause?` cause: ${e.cause.message||e.cause}`:``,n=e.code?` code: ${e.code}`:``;throw Error(`streamChat 网络错误: ${e.message}${n}${t}`)}yield{__meta:!0,logId:r.headers?.get?.(`x-tt-logid`)??``},yield*_(r.body)}};const y=`.spark`,b=`meta.json`;function x(e){return r(e,y,b)}async function S(e){try{return JSON.parse(await s(x(e),`utf8`))}catch{return null}}async function C(e){for(let t of[r(e,`..`,`..`,y,b),r(e,`..`,y,b)])try{return JSON.parse(await s(t,`utf8`))}catch{}return null}async function w(e,t){await o(r(e,y),{recursive:!0});let n=await S(e),i={...n??{},...t};if(!n){let t=await C(e);if(t)for(let[e,n]of Object.entries(t))e in i||(i[e]=n)}await c(x(e),JSON.stringify(i,null,2),`utf8`)}const T=`.spark`;function E(e){return r(e,T,`progress.txt`)}async function D(e){await o(r(e,T),{recursive:!0}),await c(E(e),``,`utf8`)}async function O(e,t){await a(E(e),`[${new Date().toISOString()}] ${t}\n`,`utf8`)}function k(e){return e?.data?.message??e?.Data?.Message??null}async function*A(e){let{cwd:t,task:n,name:r,description:i,signal:a}=e;try{let e=new v;await D(t),await O(t,`已接收需求`),yield{type:`progress`,message:`已接收需求`};let o=await S(t),s=!o?.appId,c=new Date().toISOString();if(s){let{appID:n,conversationID:a,logId:s}=await e.createSubApp({name:r,description:i});o={appId:n,conversationId:a,createdAt:c,updatedAt:c},await w(t,o),await O(t,`应用已创建(appId: ${n})`),yield{type:`app_created`,appId:n,conversationId:a,logId:s}}await O(t,`正在处理`),yield{type:`progress`,message:`正在处理`};let l=``,u=!1,d;for await(let r of e.streamChat({appID:o.appId,message:n,workDir:t})){if(a?.aborted){await O(t,`失败: cancelled`),yield{type:`failed`,error:`cancelled`};return}let e=r;if(e?.__meta){l=e.logId||``;continue}let n=k(e),i=n?.status,o=n?.type,s=n?.role,c=typeof n?.content==`string`?n.content:``,f=n?.delta?.type;!u&&s===`assistant`&&(i===`in_progress`||f===`text`)&&(u=!0,await O(t,`正在输出结果`),yield{type:`progress`,message:`正在输出结果`,logId:l||void 0}),!(s!==`assistant`||o!==`message`||i!==`completed`||!c)&&(d=c)}if(a?.aborted){await O(t,`失败: cancelled`),yield{type:`failed`,error:`cancelled`};return}let f=new Date().toISOString();await w(t,{...o,updatedAt:f,finalText:d}),await O(t,`处理完成`),yield{type:`completed`,appId:o.appId,logId:l||void 0,finalText:d}}catch(e){let n=e instanceof Error?e.message:String(e);try{await D(t),await O(t,`已接收需求`),await O(t,`失败: ${n}`)}catch{}yield{type:`failed`,error:n,logId:e?.logId}}}const j=/^[a-z0-9][a-z0-9-]{0,63}$/,M=new Set([`.`,`..`,`con`,`nul`,`prn`,`aux`]);function N(e){let t=e?.config?.plugins?.entries?.[`openclaw-extension-miaoda-coding`]?.config??{};return{verbose:t?.verbose===!0,hooks:{allowPromptInjection:t?.hooks?.allowPromptInjection!==!1}}}function P(t,r){if(!t)return;let i=n.dirname(process.execPath),a=e(`openclaw`,[`message`,`send`,`--channel`,`feishu`,`--target`,t,`--message`,r],{stdio:[`ignore`,`ignore`,`pipe`],env:{...process.env,PATH:`${i}:${process.env.PATH||``}`}});a.stderr.on(`data`,()=>{}),a.on(`close`,()=>{}),a.unref()}function F(e){return String(e??``).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,``).replace(/\s+/g,` `).trim()}function I(e){let t=String(e??``).replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/g,``).trim();if(!t)throw Error(`task 不能为空`);if(t.length>4e3)throw Error(`task 不能超过 4000 个字符`);return t}function L(e){let t=F(e);if(!R(t))throw Error(`project_id 不合法: "${t}"`);return t}function R(e){return j.test(e)&&!M.has(e)}function z(e){if(e==null||e===``)return;let t=F(e);if(!/^(user|chat|thread):\S+$/.test(t))throw Error(`target 不合法`);return t}function B(e,t,n){if(e==null||e===``)return;let r=F(e);if(r){if(r.length>n)throw Error(`${t} 不能超过 ${n} 个字符`);return r}}function V(e){try{return t.readFileSync(e,`utf8`).split(`
5
+ `).map(e=>e.trim()).filter(Boolean)}catch{return[]}}function H(e){return e.replace(/^\[[^\]]+\]\s*/,``)}function U(e){return e.replace(/应用已创建(appId:\s*[^)]+)/g,`应用已创建`)}function W(e){return e.slice(-3).map(H).map(U).join(` | `)||`暂无进度记录`}function G(e){let t=W(e);return t.includes(`失败:`)?`failed`:t.includes(`处理完成`)?`completed`:t===`暂无进度记录`?`pending`:`running`}var K={id:`openclaw-extension-miaoda-coding`,name:`妙搭`,description:`妙搭制品创建与修改`,register(e){let r=d(),i=f(r),a={verbose:!1,hooks:{allowPromptInjection:!0}};e.on(`before_prompt_build`,(e,t)=>{if(N(t).hooks.allowPromptInjection)return{prependSystemContext:[`【硬规则 — 妙搭】`,`当用户要求创建或修改应用、网页、页面时,必须通过 sessions_spawn(runtime="subagent", mode="run") 交给妙搭。`,`读 miaoda-coding skill 后严格按指示操作。只传 runtime、mode、task 三个参数,不要传 streamTo/sandbox 等。`,`绝对不要自己写代码或用 exec 生成文件。如果 spawn 失败,重试而不是 fallback。`,`不要用 sessions_history 读取子 agent 的 session,查进度用 miaoda_check_progress tool。`].join(` `)}}),e.registerTool({name:`list_projects`,label:`列出妙搭项目`,description:`列出已创建的妙搭项目,返回 project_id、appId、updatedAt、finalText、status 和进度摘要。修改已有项目时必须先调此工具。`,parameters:{type:`object`,properties:{}},async execute(){let e=[];try{e=t.readdirSync(i,{withFileTypes:!0}).filter(e=>e.isDirectory()&&R(e.name)).map(e=>{let r=n.join(i,e.name,`.spark`,`meta.json`),a=n.join(i,e.name,`.spark`,`progress.txt`),o=null;try{o=JSON.parse(t.readFileSync(r,`utf8`))}catch{}let s=V(a);return{project_id:e.name,appId:o?.appId??null,updatedAt:o?.updatedAt??null,finalText:o?.finalText??null,status:G(s),progressSummary:W(s)}})}catch{}return{content:[{type:`text`,text:JSON.stringify({projects:e},null,2)}]}}}),e.registerTool({name:`miaoda_check_progress`,label:`查看妙搭项目进度`,description:`查看妙搭项目的生成进度。用户问"做到哪了"、"进度如何"、"好了吗"时调此工具。`,parameters:{type:`object`,properties:{project_id:{type:`string`,description:`项目目录名(从 list_projects 获取)`}},required:[`project_id`]},async execute(e,r){try{let e=L(r.project_id),a=n.join(i,e,`.spark`,`progress.txt`),o=t.readFileSync(a,`utf8`).trim().split(`
6
+ `).filter(Boolean),s=null;try{s=JSON.parse(t.readFileSync(n.join(i,e,`.spark`,`meta.json`),`utf8`))}catch{}let c=!!s?.finalText,l=o.slice(-20).map(U).join(`
7
+ `);return{content:[{type:`text`,text:JSON.stringify({status:`ok`,project_id:e,lines:o.length,progress:l,delivery_ready:c,finalTextAvailable:c,note:`miaoda_check_progress 只用于查看进度,不能根据 appId 或进度文本推导预览链接;最终链接只以 subagent completion result 中的 output/finalText 为准。`})}]}}catch{return{content:[{type:`text`,text:JSON.stringify({status:`no_progress`,project_id:r.project_id,message:`暂无进度记录`,delivery_ready:!1,finalTextAvailable:!1,note:`miaoda_check_progress 只用于查看进度,不能根据 appId 或进度文本推导预览链接;最终链接只以 subagent completion result 中的 output/finalText 为准。`})}]}}}});let o=900*1e3;e.registerTool({name:`miaoda_coding`,label:`妙搭`,description:`创建或修改妙搭应用/网页。修改已有项目时必须先调 list_projects 获取 project_id,再调此工具。返回结构化 JSON(status/appId/finalText/output/meta)。`,parameters:{type:`object`,properties:{task:{type:`string`,description:`用户的需求描述(如"帮我写一个网页,展示数字 7")`},project_id:{type:`string`,description:`本地项目目录名,仅允许小写字母、数字和短横线`},target:{type:`string`,description:`飞书消息推送目标(如 user:ou_xxx)`},name:{type:`string`,description:`创建新应用时的人类可读名称`},description:{type:`string`,description:`创建新应用时的单行简介`}},required:[`task`,`project_id`]},async execute(e,i){try{let e=I(i.task),s=L(i.project_id),c=z(i.target),l=B(i.name,`name`,60),u=B(i.description,`description`,120),d=p(r,s);t.mkdirSync(d,{recursive:!0});let f=n.join(d,`.agent`);if(!t.existsSync(f)){let e=m(r);if(t.existsSync(e))try{t.symlinkSync(e,f)}catch{}}let h=[],g,_=!1,v=null,y=new AbortController,b=setTimeout(()=>y.abort(),o);try{for await(let t of A({cwd:d,task:e,name:l,description:u,signal:y.signal})){if(`logId`in t&&t.logId&&h.push(t.logId),a.verbose&&c)switch(t.type){case`app_created`:P(c,`应用已创建(appId: ${t.appId})`);break;case`failed`:P(c,`处理失败: ${t.error}`);break}t.type===`completed`&&(v={appId:t.appId,finalText:t.finalText}),t.type===`failed`&&(g=t.error)}}catch{}clearTimeout(b),_=y.signal.aborted;let x=null;try{x=JSON.parse(t.readFileSync(n.join(d,`.spark`,`meta.json`),`utf8`))}catch{}let S={status:_?`timeout`:g?`error`:`ok`,project_id:s,appId:v?.appId??x?.appId??null,finalText:v?.finalText??x?.finalText,output:v?.finalText??x?.finalText,logIds:h.length?h:void 0,meta:x};return _?S.error=`执行超时(${o/1e3}秒)`:g&&(S.error=g),{content:[{type:`text`,text:JSON.stringify(S,null,2)}]}}catch(e){return{content:[{type:`text`,text:JSON.stringify({status:`error`,error:e instanceof Error?e.message:String(e)})}]}}}}),e.registerService({id:`miaoda-coding-config`,async start(e){a=N(e)},async stop(){}})}};export{K as default};
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "openclaw-extension-miaoda-coding",
3
+ "skills": ["./skills"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {
8
+ "verbose": {
9
+ "type": "boolean",
10
+ "description": "开启后恢复中间通知,默认关闭,仅保留最终主链路回复"
11
+ },
12
+ "hooks": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "allowPromptInjection": {
17
+ "type": "boolean",
18
+ "description": "是否启用 before_prompt_build 的妙搭硬规则注入"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lark-apaas/openclaw-extension-miaoda-coding",
3
+ "version": "0.1.1-alpha.0",
4
+ "description": "妙搭 Coding Agent — 通过 AI 创建和修改妙搭应用",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "tsdown",
8
+ "build:dev": "MINIFY=false tsdown",
9
+ "test": "vitest run",
10
+ "test:cov": "vitest --coverage",
11
+ "ci": "npm run test"
12
+ },
13
+ "files": [
14
+ "dist/",
15
+ "skills/",
16
+ "openclaw.plugin.json"
17
+ ],
18
+ "openclaw": {
19
+ "extensions": [
20
+ "./dist/index.mjs"
21
+ ],
22
+ "skills": [
23
+ "./skills"
24
+ ]
25
+ },
26
+ "dependencies": {
27
+ "@lark-apaas/http-client": "^0.1.2"
28
+ },
29
+ "devDependencies": {
30
+ "@vitest/coverage-v8": "^4.0.18",
31
+ "openclaw": "^2026.3.2",
32
+ "tsdown": "^0.21.0",
33
+ "vitest": "^4.0.18"
34
+ }
35
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: miaoda-coding
3
+ description: 当用户想创建或修改任何东西(应用、网页、页面、PPT、幻灯片、演示文稿)或提到"妙搭"、"vibe coding"、"做一个"、"帮我写"、"加个"、"修改"、"更新"、"改一下"、"优化"时,必须读此 skill 后严格按指示操作。绝对不要自己写代码或 exec 命令,必须通过 sessions_spawn + miaoda_coding tool 完成。
4
+ user-invocable: false
5
+ ---
6
+
7
+ # 妙搭
8
+
9
+ **重要:任何涉及创建或修改应用、网页、代码、PPT、幻灯片、演示文稿的请求,都必须通过 sessions_spawn 交给妙搭。绝对不要自己动手写代码。**
10
+
11
+ ## 派活
12
+
13
+ 通过 `sessions_spawn` 派给 subagent。**只传以下三个参数,不要传任何其他参数**(不要传 streamTo、sandbox、stream 等):
14
+
15
+ - `runtime`: `"subagent"`
16
+ - `mode`: `"run"`
17
+ - `task`: 按下面模板填写(替换 `<用户需求>`、`<sender_id>` 和 `<project_id>`)
18
+
19
+ 如果 sessions_spawn 返回错误,去掉多余参数后重试,**绝对不要 fallback 到自己写代码**。
20
+
21
+ **创建新项目:**
22
+ ```
23
+ 调用 miaoda_coding tool,参数:
24
+ - task: "<用户需求>"
25
+ - project_id: "<project_id>"
26
+ - name: "<面向人类可读的应用名称>"
27
+ - description: "<根据用户需求整理的一句话简介,单行,80 字以内>"
28
+ - target: "user:<sender_id>"
29
+
30
+ tool 会返回结构化 JSON(status/appId/finalText/output 等)。
31
+
32
+ 如果结果里有 `output`,announce 给主 agent 时优先带 `output` 原文,不要自己改写预览链接,不要提前拼 `mode=sidebar-semi`。
33
+
34
+ 不要调 message tool(主 agent 会处理消息投递)。
35
+ ```
36
+
37
+ **修改已有项目:**
38
+ ```
39
+ 你只能调用以下两个 tool,按顺序执行,不得使用任何其他 tool(exec、ls、read 等均禁止):
40
+
41
+ 1. 调用 list_projects tool,无需任何参数。
42
+ 2. 从返回的 projects 数组中找到与用户需求匹配的项目,取其 project_id。
43
+ 3. 调用 miaoda_coding tool,参数:
44
+ - task: "<用户需求>"
45
+ - project_id: "<上一步取到的 project_id>"
46
+ - target: "user:<sender_id>"
47
+
48
+ tool 会返回结构化 JSON(status/appId/finalText/output 等)。
49
+
50
+ 如果结果里有 `output`,announce 给主 agent 时优先带 `output` 原文,不要自己改写预览链接,不要提前拼 `mode=sidebar-semi`。
51
+
52
+ 不要调 message tool(主 agent 会处理消息投递)。
53
+ ```
54
+
55
+ `<sender_id>` 从消息上下文的 sender_id 字段获取(格式如 `ou_xxx`)。
56
+
57
+ `<project_id>` 仅允许小写字母、数字和短横线,创建新项目时根据用户需求生成,例如:
58
+ - "帮我做一个 hello world 网页" → `hello-world-webpage`
59
+ - "做一个计算器" → `calculator`
60
+ - "做一个贪吃蛇游戏" → `snake-game`
61
+
62
+ ## 你(主 agent)的行为
63
+
64
+ 1. 读完这个 skill 后,只做一件事:调 sessions_spawn
65
+ 2. 回复用户"交给妙搭了,稍等"
66
+ 3. **不要** 调 sessions_history、subagents、或任何 poll 操作
67
+ 4. **不要** 自己执行任何命令
68
+ 5. subagent announce 回来后,如果结果里有 `output` / `finalText`:
69
+ - 优先从其中提取 `预览链接:<url>`
70
+ - 由主 agent 自己给这个链接补 `mode=sidebar-semi`
71
+ - 再组织最终回复给用户
72
+ 6. `miaoda_check_progress` 只用于查看进度,绝对不要根据 `appId`、进度文本或固定域名模式自己猜测预览链接
73
+ 7. 如果 subagent completion event 还没到,即使进度里出现“处理完成”,也只能回复进度状态,不能提前发最终链接
74
+ 8. 如果没找到预览链接,就按无链接结果回复,不要猜
75
+ 9. 收到 announce 后回复 `NO_REPLY`(最终用户消息由主 agent 统一处理)