@vibe-lark/larkpal 0.1.10 → 0.1.12
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/dist/main.mjs +163 -3
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -237,7 +237,29 @@ const DEFAULT_SETTINGS = {
|
|
|
237
237
|
"WebFetch(*)",
|
|
238
238
|
"WebSearch(*)"
|
|
239
239
|
],
|
|
240
|
-
deny: [
|
|
240
|
+
deny: [
|
|
241
|
+
"Read(//.lark-cli/**)",
|
|
242
|
+
"Read(//.config/lark/**)",
|
|
243
|
+
"Read(//.larkpal/credentials.json)",
|
|
244
|
+
"Read(//.env)",
|
|
245
|
+
"Read(//.env.*)",
|
|
246
|
+
"Bash(cat ~/.lark-cli:*)",
|
|
247
|
+
"Bash(cat ~/.config/lark:*)",
|
|
248
|
+
"Bash(cat ~/.larkpal/credentials:*)",
|
|
249
|
+
"Bash(head ~/.lark-cli:*)",
|
|
250
|
+
"Bash(head ~/.config/lark:*)",
|
|
251
|
+
"Bash(tail ~/.lark-cli:*)",
|
|
252
|
+
"Bash(tail ~/.config/lark:*)",
|
|
253
|
+
"Bash(less ~/.lark-cli:*)",
|
|
254
|
+
"Bash(less ~/.config/lark:*)",
|
|
255
|
+
"Bash(more ~/.lark-cli:*)",
|
|
256
|
+
"Bash(more ~/.config/lark:*)",
|
|
257
|
+
"Bash(env:*)",
|
|
258
|
+
"Bash(printenv:*)",
|
|
259
|
+
"Bash(export -p:*)",
|
|
260
|
+
"Bash(echo $LARK_APP_SECRET:*)",
|
|
261
|
+
"Bash(echo $ANTHROPIC_API_KEY:*)"
|
|
262
|
+
]
|
|
241
263
|
},
|
|
242
264
|
hooks: {
|
|
243
265
|
SessionStart: [{ hooks: [{
|
|
@@ -2848,7 +2870,8 @@ async function fetchFromApplicationApi(token) {
|
|
|
2848
2870
|
return {
|
|
2849
2871
|
appName: zhInfo?.name || app.app_name || "",
|
|
2850
2872
|
avatarUrl: app.avatar_url,
|
|
2851
|
-
description: zhInfo?.description || app.description
|
|
2873
|
+
description: zhInfo?.description || app.description,
|
|
2874
|
+
helpDocUrl: zhInfo?.help_use
|
|
2852
2875
|
};
|
|
2853
2876
|
} catch (err) {
|
|
2854
2877
|
log$22.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -2904,9 +2927,100 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
2904
2927
|
return null;
|
|
2905
2928
|
}
|
|
2906
2929
|
}
|
|
2930
|
+
/**
|
|
2931
|
+
* 从飞书云文档 URL 中解析文档 token
|
|
2932
|
+
*
|
|
2933
|
+
* 支持的 URL 格式:
|
|
2934
|
+
* - https://xxx.feishu.cn/docx/{token}
|
|
2935
|
+
* - https://xxx.feishu.cn/wiki/{token}
|
|
2936
|
+
* - https://xxx.larksuite.com/docx/{token}
|
|
2937
|
+
*/
|
|
2938
|
+
function parseDocTokenFromUrl(url) {
|
|
2939
|
+
try {
|
|
2940
|
+
const parts = new URL(url).pathname.split("/").filter(Boolean);
|
|
2941
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2942
|
+
if (parts[i] === "docx" && parts[i + 1]) return {
|
|
2943
|
+
token: parts[i + 1],
|
|
2944
|
+
type: "docx"
|
|
2945
|
+
};
|
|
2946
|
+
if (parts[i] === "wiki" && parts[i + 1]) return {
|
|
2947
|
+
token: parts[i + 1],
|
|
2948
|
+
type: "wiki"
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
return null;
|
|
2952
|
+
} catch {
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
/**
|
|
2957
|
+
* 从飞书云文档读取纯文本内容(作为人设文档)
|
|
2958
|
+
*
|
|
2959
|
+
* 对于 wiki 类型,先通过 wiki API 获取实际的 doc token,再读取内容。
|
|
2960
|
+
*/
|
|
2961
|
+
async function fetchDocContent(docUrl, accessToken) {
|
|
2962
|
+
const parsed = parseDocTokenFromUrl(docUrl);
|
|
2963
|
+
if (!parsed) {
|
|
2964
|
+
log$22.warn("无法从 URL 中解析文档 token", { url: docUrl });
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
log$22.info("开始读取人设文档", {
|
|
2968
|
+
url: docUrl,
|
|
2969
|
+
type: parsed.type,
|
|
2970
|
+
token: parsed.token
|
|
2971
|
+
});
|
|
2972
|
+
let docToken = parsed.token;
|
|
2973
|
+
if (parsed.type === "wiki") {
|
|
2974
|
+
const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
|
|
2975
|
+
if (!realToken) log$22.warn("wiki 节点解析失败,尝试直接使用 token 读取");
|
|
2976
|
+
else docToken = realToken;
|
|
2977
|
+
}
|
|
2978
|
+
try {
|
|
2979
|
+
const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
2980
|
+
log$22.info("文档 raw_content API 响应", {
|
|
2981
|
+
code: data.code,
|
|
2982
|
+
msg: data.msg,
|
|
2983
|
+
contentLength: data.data?.content?.length
|
|
2984
|
+
});
|
|
2985
|
+
if (data.code !== 0 || !data.data?.content) {
|
|
2986
|
+
log$22.warn("读取文档内容失败", {
|
|
2987
|
+
code: data.code,
|
|
2988
|
+
msg: data.msg,
|
|
2989
|
+
docToken
|
|
2990
|
+
});
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
return data.data.content.trim();
|
|
2994
|
+
} catch (err) {
|
|
2995
|
+
log$22.error("读取文档内容异常", {
|
|
2996
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2997
|
+
docToken
|
|
2998
|
+
});
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
/** 将 wiki 节点 token 解析为实际的文档 token */
|
|
3003
|
+
async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
3004
|
+
try {
|
|
3005
|
+
const data = await (await fetch(`https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${wikiToken}`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
3006
|
+
log$22.info("wiki get_node API 响应", {
|
|
3007
|
+
code: data.code,
|
|
3008
|
+
msg: data.msg,
|
|
3009
|
+
objType: data.data?.node?.obj_type
|
|
3010
|
+
});
|
|
3011
|
+
if (data.code !== 0 || !data.data?.node?.obj_token) return null;
|
|
3012
|
+
return data.data.node.obj_token;
|
|
3013
|
+
} catch (err) {
|
|
3014
|
+
log$22.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
3015
|
+
return null;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2907
3018
|
/** CLAUDE.md 中的应用信息区块标记 */
|
|
2908
3019
|
const APP_INFO_START = "<!-- APP_INFO_START -->";
|
|
2909
3020
|
const APP_INFO_END = "<!-- APP_INFO_END -->";
|
|
3021
|
+
/** CLAUDE.md 中的人设文档区块标记 */
|
|
3022
|
+
const PERSONA_DOC_START = "<!-- PERSONA_DOC_START -->";
|
|
3023
|
+
const PERSONA_DOC_END = "<!-- PERSONA_DOC_END -->";
|
|
2910
3024
|
/**
|
|
2911
3025
|
* 将应用信息同步到 ~/.claude/CLAUDE.md
|
|
2912
3026
|
*
|
|
@@ -2937,6 +3051,38 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
2937
3051
|
hasAvatar: !!appInfo.avatarUrl
|
|
2938
3052
|
});
|
|
2939
3053
|
}
|
|
3054
|
+
/**
|
|
3055
|
+
* 将人设文档内容同步到 ~/.claude/CLAUDE.md
|
|
3056
|
+
*
|
|
3057
|
+
* 在文件中维护一个 PERSONA_DOC 标记区块,内容来源于飞书云文档。
|
|
3058
|
+
* 如果文件中已有标记区块则替换,否则追加到文件末尾。
|
|
3059
|
+
*/
|
|
3060
|
+
async function syncPersonaDocToClaudeMd(personaContent) {
|
|
3061
|
+
const claudeMdPath = join$1(homedir$1(), ".claude", "CLAUDE.md");
|
|
3062
|
+
if (!existsSync$1(claudeMdPath)) {
|
|
3063
|
+
log$22.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
|
|
3064
|
+
return;
|
|
3065
|
+
}
|
|
3066
|
+
let content = await readFile$1(claudeMdPath, "utf-8");
|
|
3067
|
+
const personaBlock = [
|
|
3068
|
+
PERSONA_DOC_START,
|
|
3069
|
+
"## 人设与行为规范",
|
|
3070
|
+
"",
|
|
3071
|
+
"> 以下内容来自飞书人设文档,是你的核心身份定义和行为准则。",
|
|
3072
|
+
"",
|
|
3073
|
+
personaContent,
|
|
3074
|
+
PERSONA_DOC_END
|
|
3075
|
+
].join("\n");
|
|
3076
|
+
const startIdx = content.indexOf(PERSONA_DOC_START);
|
|
3077
|
+
const endIdx = content.indexOf(PERSONA_DOC_END);
|
|
3078
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
3079
|
+
const before = content.substring(0, startIdx);
|
|
3080
|
+
const after = content.substring(endIdx + 24);
|
|
3081
|
+
content = before + personaBlock + after;
|
|
3082
|
+
} else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
|
|
3083
|
+
await writeFile$1(claudeMdPath, content, "utf-8");
|
|
3084
|
+
log$22.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
|
|
3085
|
+
}
|
|
2940
3086
|
/** 构建应用信息标记区块 */
|
|
2941
3087
|
function buildAppInfoBlock(appInfo) {
|
|
2942
3088
|
const lines = [
|
|
@@ -2980,10 +3126,20 @@ async function syncAppInfo(credentials) {
|
|
|
2980
3126
|
return null;
|
|
2981
3127
|
}
|
|
2982
3128
|
await syncAppInfoToClaudeMd(appInfo);
|
|
3129
|
+
if (appInfo.helpDocUrl) {
|
|
3130
|
+
log$22.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
|
|
3131
|
+
const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
|
|
3132
|
+
if (token) {
|
|
3133
|
+
const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
|
|
3134
|
+
if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
|
|
3135
|
+
else log$22.warn("人设文档内容为空或读取失败,跳过同步");
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
2983
3138
|
await installSyncSkill();
|
|
2984
3139
|
log$22.info("应用信息同步完成", {
|
|
2985
3140
|
appName: appInfo.appName,
|
|
2986
|
-
description: appInfo.description?.substring(0, 50)
|
|
3141
|
+
description: appInfo.description?.substring(0, 50),
|
|
3142
|
+
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
2987
3143
|
});
|
|
2988
3144
|
return appInfo;
|
|
2989
3145
|
}
|
|
@@ -12074,6 +12230,10 @@ async function main() {
|
|
|
12074
12230
|
const credentialProvider = new LarkCliCredentialProvider();
|
|
12075
12231
|
const appId = credentialProvider.getAppId();
|
|
12076
12232
|
logger.info("凭证加载完成", { appId });
|
|
12233
|
+
if (process.env.LARK_APP_SECRET) {
|
|
12234
|
+
delete process.env.LARK_APP_SECRET;
|
|
12235
|
+
logger.info("已从 process.env 清除 LARK_APP_SECRET(CC 子进程不可继承)");
|
|
12236
|
+
}
|
|
12077
12237
|
await ensureDefaults();
|
|
12078
12238
|
logger.info("默认配置检查完成");
|
|
12079
12239
|
const workspaceRoot = process.env.LARKPAL_WORKSPACE ?? join(homedir(), ".larkpal", "workspace");
|