@vibe-lark/larkpal 0.1.11 → 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 +136 -2
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -2870,7 +2870,8 @@ async function fetchFromApplicationApi(token) {
|
|
|
2870
2870
|
return {
|
|
2871
2871
|
appName: zhInfo?.name || app.app_name || "",
|
|
2872
2872
|
avatarUrl: app.avatar_url,
|
|
2873
|
-
description: zhInfo?.description || app.description
|
|
2873
|
+
description: zhInfo?.description || app.description,
|
|
2874
|
+
helpDocUrl: zhInfo?.help_use
|
|
2874
2875
|
};
|
|
2875
2876
|
} catch (err) {
|
|
2876
2877
|
log$22.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -2926,9 +2927,100 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
2926
2927
|
return null;
|
|
2927
2928
|
}
|
|
2928
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
|
+
}
|
|
2929
3018
|
/** CLAUDE.md 中的应用信息区块标记 */
|
|
2930
3019
|
const APP_INFO_START = "<!-- APP_INFO_START -->";
|
|
2931
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 -->";
|
|
2932
3024
|
/**
|
|
2933
3025
|
* 将应用信息同步到 ~/.claude/CLAUDE.md
|
|
2934
3026
|
*
|
|
@@ -2959,6 +3051,38 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
2959
3051
|
hasAvatar: !!appInfo.avatarUrl
|
|
2960
3052
|
});
|
|
2961
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
|
+
}
|
|
2962
3086
|
/** 构建应用信息标记区块 */
|
|
2963
3087
|
function buildAppInfoBlock(appInfo) {
|
|
2964
3088
|
const lines = [
|
|
@@ -3002,10 +3126,20 @@ async function syncAppInfo(credentials) {
|
|
|
3002
3126
|
return null;
|
|
3003
3127
|
}
|
|
3004
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
|
+
}
|
|
3005
3138
|
await installSyncSkill();
|
|
3006
3139
|
log$22.info("应用信息同步完成", {
|
|
3007
3140
|
appName: appInfo.appName,
|
|
3008
|
-
description: appInfo.description?.substring(0, 50)
|
|
3141
|
+
description: appInfo.description?.substring(0, 50),
|
|
3142
|
+
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
3009
3143
|
});
|
|
3010
3144
|
return appInfo;
|
|
3011
3145
|
}
|