@vibe-lark/larkpal 0.1.11 → 0.1.13
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 +149 -5
- package/package.json +1 -1
package/dist/main.mjs
CHANGED
|
@@ -1029,18 +1029,28 @@ var SessionProcessManager = class {
|
|
|
1029
1029
|
processEnv.LARKPAL_USER_NAME = userContext.userName;
|
|
1030
1030
|
processEnv.LARKPAL_CREDENTIAL_DIR = userContext.credentialDir;
|
|
1031
1031
|
processEnv.LARKPAL_IS_TENANT = userContext.isTenantIdentity ? "1" : "0";
|
|
1032
|
+
const disableUserAuth = process.env.LARKPAL_DISABLE_USER_AUTH === "1" || process.env.LARKPAL_DISABLE_USER_AUTH === "true";
|
|
1032
1033
|
if (!userContext.isTenantIdentity) try {
|
|
1033
1034
|
const vault = new CredentialVault(userContext.credentialDir);
|
|
1034
1035
|
await vault.load();
|
|
1035
1036
|
const credentialEnvVars = await vault.getAllCredentialsAsEnv();
|
|
1036
1037
|
Object.assign(processEnv, credentialEnvVars);
|
|
1037
|
-
|
|
1038
|
-
|
|
1038
|
+
if (!disableUserAuth) {
|
|
1039
|
+
const larkCred = await vault.getCredential("lark");
|
|
1040
|
+
if (larkCred?.user_access_token && typeof larkCred.user_access_token === "string") processEnv.LARK_USER_ACCESS_TOKEN = larkCred.user_access_token;
|
|
1041
|
+
} else {
|
|
1042
|
+
delete processEnv.LARK_USER_ACCESS_TOKEN;
|
|
1043
|
+
log$25.info("LARKPAL_DISABLE_USER_AUTH 已启用,跳过飞书 user_access_token 注入", {
|
|
1044
|
+
sessionId,
|
|
1045
|
+
userId: userContext.userId
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1039
1048
|
log$25.info("用户凭证已注入到 CC 进程环境", {
|
|
1040
1049
|
sessionId,
|
|
1041
1050
|
userId: userContext.userId,
|
|
1042
1051
|
credentialCount: Object.keys(credentialEnvVars).length,
|
|
1043
|
-
hasLarkUserToken: !!processEnv.LARK_USER_ACCESS_TOKEN
|
|
1052
|
+
hasLarkUserToken: !!processEnv.LARK_USER_ACCESS_TOKEN,
|
|
1053
|
+
disableUserAuth
|
|
1044
1054
|
});
|
|
1045
1055
|
} catch (err) {
|
|
1046
1056
|
log$25.warn("用户凭证加载失败,CC 进程将使用默认环境", {
|
|
@@ -2870,7 +2880,8 @@ async function fetchFromApplicationApi(token) {
|
|
|
2870
2880
|
return {
|
|
2871
2881
|
appName: zhInfo?.name || app.app_name || "",
|
|
2872
2882
|
avatarUrl: app.avatar_url,
|
|
2873
|
-
description: zhInfo?.description || app.description
|
|
2883
|
+
description: zhInfo?.description || app.description,
|
|
2884
|
+
helpDocUrl: zhInfo?.help_use
|
|
2874
2885
|
};
|
|
2875
2886
|
} catch (err) {
|
|
2876
2887
|
log$22.warn("application/v6 API 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
@@ -2926,9 +2937,100 @@ async function getTenantAccessToken(appId, appSecret) {
|
|
|
2926
2937
|
return null;
|
|
2927
2938
|
}
|
|
2928
2939
|
}
|
|
2940
|
+
/**
|
|
2941
|
+
* 从飞书云文档 URL 中解析文档 token
|
|
2942
|
+
*
|
|
2943
|
+
* 支持的 URL 格式:
|
|
2944
|
+
* - https://xxx.feishu.cn/docx/{token}
|
|
2945
|
+
* - https://xxx.feishu.cn/wiki/{token}
|
|
2946
|
+
* - https://xxx.larksuite.com/docx/{token}
|
|
2947
|
+
*/
|
|
2948
|
+
function parseDocTokenFromUrl(url) {
|
|
2949
|
+
try {
|
|
2950
|
+
const parts = new URL(url).pathname.split("/").filter(Boolean);
|
|
2951
|
+
for (let i = 0; i < parts.length; i++) {
|
|
2952
|
+
if (parts[i] === "docx" && parts[i + 1]) return {
|
|
2953
|
+
token: parts[i + 1],
|
|
2954
|
+
type: "docx"
|
|
2955
|
+
};
|
|
2956
|
+
if (parts[i] === "wiki" && parts[i + 1]) return {
|
|
2957
|
+
token: parts[i + 1],
|
|
2958
|
+
type: "wiki"
|
|
2959
|
+
};
|
|
2960
|
+
}
|
|
2961
|
+
return null;
|
|
2962
|
+
} catch {
|
|
2963
|
+
return null;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
/**
|
|
2967
|
+
* 从飞书云文档读取纯文本内容(作为人设文档)
|
|
2968
|
+
*
|
|
2969
|
+
* 对于 wiki 类型,先通过 wiki API 获取实际的 doc token,再读取内容。
|
|
2970
|
+
*/
|
|
2971
|
+
async function fetchDocContent(docUrl, accessToken) {
|
|
2972
|
+
const parsed = parseDocTokenFromUrl(docUrl);
|
|
2973
|
+
if (!parsed) {
|
|
2974
|
+
log$22.warn("无法从 URL 中解析文档 token", { url: docUrl });
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
log$22.info("开始读取人设文档", {
|
|
2978
|
+
url: docUrl,
|
|
2979
|
+
type: parsed.type,
|
|
2980
|
+
token: parsed.token
|
|
2981
|
+
});
|
|
2982
|
+
let docToken = parsed.token;
|
|
2983
|
+
if (parsed.type === "wiki") {
|
|
2984
|
+
const realToken = await resolveWikiNodeToDocToken(parsed.token, accessToken);
|
|
2985
|
+
if (!realToken) log$22.warn("wiki 节点解析失败,尝试直接使用 token 读取");
|
|
2986
|
+
else docToken = realToken;
|
|
2987
|
+
}
|
|
2988
|
+
try {
|
|
2989
|
+
const data = await (await fetch(`https://open.feishu.cn/open-apis/docx/v1/documents/${docToken}/raw_content`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
2990
|
+
log$22.info("文档 raw_content API 响应", {
|
|
2991
|
+
code: data.code,
|
|
2992
|
+
msg: data.msg,
|
|
2993
|
+
contentLength: data.data?.content?.length
|
|
2994
|
+
});
|
|
2995
|
+
if (data.code !== 0 || !data.data?.content) {
|
|
2996
|
+
log$22.warn("读取文档内容失败", {
|
|
2997
|
+
code: data.code,
|
|
2998
|
+
msg: data.msg,
|
|
2999
|
+
docToken
|
|
3000
|
+
});
|
|
3001
|
+
return null;
|
|
3002
|
+
}
|
|
3003
|
+
return data.data.content.trim();
|
|
3004
|
+
} catch (err) {
|
|
3005
|
+
log$22.error("读取文档内容异常", {
|
|
3006
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3007
|
+
docToken
|
|
3008
|
+
});
|
|
3009
|
+
return null;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
/** 将 wiki 节点 token 解析为实际的文档 token */
|
|
3013
|
+
async function resolveWikiNodeToDocToken(wikiToken, accessToken) {
|
|
3014
|
+
try {
|
|
3015
|
+
const data = await (await fetch(`https://open.feishu.cn/open-apis/wiki/v2/spaces/get_node?token=${wikiToken}`, { headers: { Authorization: `Bearer ${accessToken}` } })).json();
|
|
3016
|
+
log$22.info("wiki get_node API 响应", {
|
|
3017
|
+
code: data.code,
|
|
3018
|
+
msg: data.msg,
|
|
3019
|
+
objType: data.data?.node?.obj_type
|
|
3020
|
+
});
|
|
3021
|
+
if (data.code !== 0 || !data.data?.node?.obj_token) return null;
|
|
3022
|
+
return data.data.node.obj_token;
|
|
3023
|
+
} catch (err) {
|
|
3024
|
+
log$22.warn("wiki get_node 请求异常", { error: err instanceof Error ? err.message : String(err) });
|
|
3025
|
+
return null;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
2929
3028
|
/** CLAUDE.md 中的应用信息区块标记 */
|
|
2930
3029
|
const APP_INFO_START = "<!-- APP_INFO_START -->";
|
|
2931
3030
|
const APP_INFO_END = "<!-- APP_INFO_END -->";
|
|
3031
|
+
/** CLAUDE.md 中的人设文档区块标记 */
|
|
3032
|
+
const PERSONA_DOC_START = "<!-- PERSONA_DOC_START -->";
|
|
3033
|
+
const PERSONA_DOC_END = "<!-- PERSONA_DOC_END -->";
|
|
2932
3034
|
/**
|
|
2933
3035
|
* 将应用信息同步到 ~/.claude/CLAUDE.md
|
|
2934
3036
|
*
|
|
@@ -2959,6 +3061,38 @@ async function syncAppInfoToClaudeMd(appInfo) {
|
|
|
2959
3061
|
hasAvatar: !!appInfo.avatarUrl
|
|
2960
3062
|
});
|
|
2961
3063
|
}
|
|
3064
|
+
/**
|
|
3065
|
+
* 将人设文档内容同步到 ~/.claude/CLAUDE.md
|
|
3066
|
+
*
|
|
3067
|
+
* 在文件中维护一个 PERSONA_DOC 标记区块,内容来源于飞书云文档。
|
|
3068
|
+
* 如果文件中已有标记区块则替换,否则追加到文件末尾。
|
|
3069
|
+
*/
|
|
3070
|
+
async function syncPersonaDocToClaudeMd(personaContent) {
|
|
3071
|
+
const claudeMdPath = join$1(homedir$1(), ".claude", "CLAUDE.md");
|
|
3072
|
+
if (!existsSync$1(claudeMdPath)) {
|
|
3073
|
+
log$22.warn("CLAUDE.md 不存在,无法同步人设文档(需先同步应用信息)");
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
let content = await readFile$1(claudeMdPath, "utf-8");
|
|
3077
|
+
const personaBlock = [
|
|
3078
|
+
PERSONA_DOC_START,
|
|
3079
|
+
"## 人设与行为规范",
|
|
3080
|
+
"",
|
|
3081
|
+
"> 以下内容来自飞书人设文档,是你的核心身份定义和行为准则。",
|
|
3082
|
+
"",
|
|
3083
|
+
personaContent,
|
|
3084
|
+
PERSONA_DOC_END
|
|
3085
|
+
].join("\n");
|
|
3086
|
+
const startIdx = content.indexOf(PERSONA_DOC_START);
|
|
3087
|
+
const endIdx = content.indexOf(PERSONA_DOC_END);
|
|
3088
|
+
if (startIdx !== -1 && endIdx !== -1) {
|
|
3089
|
+
const before = content.substring(0, startIdx);
|
|
3090
|
+
const after = content.substring(endIdx + 24);
|
|
3091
|
+
content = before + personaBlock + after;
|
|
3092
|
+
} else content = content.trimEnd() + "\n\n" + personaBlock + "\n";
|
|
3093
|
+
await writeFile$1(claudeMdPath, content, "utf-8");
|
|
3094
|
+
log$22.info("CLAUDE.md 人设文档已同步", { contentLength: personaContent.length });
|
|
3095
|
+
}
|
|
2962
3096
|
/** 构建应用信息标记区块 */
|
|
2963
3097
|
function buildAppInfoBlock(appInfo) {
|
|
2964
3098
|
const lines = [
|
|
@@ -3002,10 +3136,20 @@ async function syncAppInfo(credentials) {
|
|
|
3002
3136
|
return null;
|
|
3003
3137
|
}
|
|
3004
3138
|
await syncAppInfoToClaudeMd(appInfo);
|
|
3139
|
+
if (appInfo.helpDocUrl) {
|
|
3140
|
+
log$22.info("检测到帮助文档 URL,尝试同步人设文档", { helpDocUrl: appInfo.helpDocUrl });
|
|
3141
|
+
const token = await getTenantAccessToken(credentials.appId, credentials.appSecret);
|
|
3142
|
+
if (token) {
|
|
3143
|
+
const personaContent = await fetchDocContent(appInfo.helpDocUrl, token);
|
|
3144
|
+
if (personaContent) await syncPersonaDocToClaudeMd(personaContent);
|
|
3145
|
+
else log$22.warn("人设文档内容为空或读取失败,跳过同步");
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3005
3148
|
await installSyncSkill();
|
|
3006
3149
|
log$22.info("应用信息同步完成", {
|
|
3007
3150
|
appName: appInfo.appName,
|
|
3008
|
-
description: appInfo.description?.substring(0, 50)
|
|
3151
|
+
description: appInfo.description?.substring(0, 50),
|
|
3152
|
+
hasPersonaDoc: !!appInfo.helpDocUrl
|
|
3009
3153
|
});
|
|
3010
3154
|
return appInfo;
|
|
3011
3155
|
}
|