Compare commits

...

7 Commits

Author SHA1 Message Date
titanwings
ce30311a72 fix: replace broken /search/v1/user with department traversal + batch_get_id
The old find_user() used /search/v1/user which requires user_access_token,
but the code only has tenant_access_token, causing error 99991663 every time.

New strategy:
1. Email/phone → /contact/v3/users/batch_get_id (fastest, proven working)
2. Name → department traversal + find_by_department (needs contact:department.base:readonly)
3. Both fail → clear error message with suggestions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 19:10:48 +08:00
titanwings
3e9cc07b62 docs: add WeChat to data sources, clarify recommended export tools
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 15:30:39 +08:00
titanwings
be54d25f03 docs: remove instability wording from chat export tools section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 15:17:38 +08:00
titanwings
3db7e8d83e docs: add recommended WeChat chat export tools section
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 15:09:24 +08:00
titanwings
c276624214 docs: sync README_EN.md with Chinese version
- Add Slack row to data sources table
- Add ex-skill cross-promotion section
- Add slack_auto_collector.py to project structure
- Update intro text to mention Slack

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 13:23:38 +08:00
titanwings
804025553a docs: move ex-skill promo above data sources table
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 12:51:15 +08:00
titanwings
7aff711793 docs: add cross-promotion for ex-skill in README
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-01 12:50:09 +08:00
3 changed files with 205 additions and 17 deletions

View File

@@ -31,6 +31,22 @@
---
### 🌟 同系列项目:[前任.skill](https://github.com/titanwings/ex-skill)
> 根据大家的 issue 反馈,更新了一版 **前任.skill**!现已支持:
>
> - **微信聊天记录全自动导入**Windows / macOS桌面端登录即可无需任何配置
> - **iMessage 全自动提取**macOS 用户)
> - **完整星盘解读**(太阳/月亮/上升/金星/火星/水星 × 12 星座)
> - **MBTI 16 型 + 认知功能**、九型人格、依恋风格全支持
> - 支持所有性别认同与关系类型
>
> 同事跑了用 **同事.skill**,前任跑了用 **[前任.skill](https://github.com/titanwings/ex-skill)**,赛博永生一条龙 🌟🌟🌟
>
> 觉得有意思的话,给两个项目都点个 Star 吧!
---
## 支持的数据来源
> 目前还是同事.skill 的 beta 测试版本,后续会有更多来源支持,请多多关注!
@@ -40,6 +56,7 @@
| 飞书(自动采集) | ✅ API | ✅ | ✅ | 输入姓名即可,全自动 |
| 钉钉(自动采集) | ⚠️ 浏览器 | ✅ | ✅ | 钉钉 API 不支持历史消息 |
| Slack自动采集 | ✅ API | — | — | 需管理员安装 Bot免费版限 90 天 |
| 微信聊天记录 | ✅ SQLite | — | — | 目前测试下来不太稳定,推荐先用下方开源工具代替 |
| PDF | — | ✅ | — | 手动上传 |
| 图片 / 截图 | ✅ | — | — | 手动上传 |
| 飞书 JSON 导出 | ✅ | ✅ | — | 手动上传 |
@@ -47,6 +64,18 @@
| Markdown | ✅ | ✅ | — | 手动上传 |
| 直接粘贴文字 | ✅ | — | — | 手动输入 |
### 推荐的微信聊天记录导出工具
以下工具为独立的开源项目,本项目不包含它们的代码,仅在解析器中适配了它们的导出格式。目前微信自动解密测试下来不太稳定,可以先用这些开源工具导出聊天记录,再粘贴或导入到本项目中使用:
| 工具 | 平台 | 说明 |
|------|------|------|
| [WeChatMsg](https://github.com/LC044/WeChatMsg) | Windows | 微信聊天记录导出,支持多种格式 |
| [PyWxDump](https://github.com/xaoyaoo/PyWxDump) | Windows | 微信数据库解密导出 |
| [留痕](https://github.com/greyovo/留痕) | macOS | 微信聊天记录导出Mac 用户推荐) |
> 工具信息来自 [@therealXiaomanChu](https://github.com/therealXiaomanChu),感谢各位开源作者,一起助力赛博永生!
---
## 安装

View File

@@ -21,7 +21,7 @@ Your predecessor handed over, trying to condense three years into three pages?<b
<br>
Provide source materials (Feishu messages, DingTalk docs, emails, screenshots)<br>
Provide source materials (Feishu messages, DingTalk docs, Slack messages, emails, screenshots)<br>
plus your subjective description of the person<br>
and get an **AI Skill that actually works like them**
@@ -31,6 +31,22 @@ and get an **AI Skill that actually works like them**
---
### 🌟 Related Project: [ex.skill](https://github.com/titanwings/ex-skill)
> Based on community feedback, we've updated **ex.skill** (a.k.a. 前任.skill)! Now supports:
>
> - **WeChat chat history auto-import** (Windows / macOS, just keep WeChat desktop logged in, zero config)
> - **iMessage auto-extraction** (macOS users)
> - **Full astrology chart interpretation** (Sun/Moon/Rising/Venus/Mars/Mercury × 12 signs)
> - **MBTI 16 types + cognitive functions**, Enneagram, attachment styles
> - All gender identities and relationship types supported
>
> Colleague left? Use **colleague.skill**. Ex left? Use **[ex.skill](https://github.com/titanwings/ex-skill)**. Cyber-immortality, end to end 🌟🌟🌟
>
> If you find this interesting, give both projects a Star!
---
## Supported Data Sources
> This is still a beta version of colleague.skill — more sources coming soon, stay tuned!
@@ -39,6 +55,8 @@ and get an **AI Skill that actually works like them**
|--------|:--------:|:-----------:|:------------:|-------|
| Feishu (auto) | ✅ API | ✅ | ✅ | Just enter a name, fully automatic |
| DingTalk (auto) | ⚠️ Browser | ✅ | ✅ | DingTalk API doesn't support message history |
| Slack (auto) | ✅ API | — | — | Requires admin to install Bot; free plan limited to 90 days |
| WeChat chat history | ✅ SQLite | — | — | Currently unstable, recommend using open-source tools below |
| PDF | — | ✅ | — | Manual upload |
| Images / Screenshots | ✅ | — | — | Manual upload |
| Feishu JSON export | ✅ | ✅ | — | Manual upload |
@@ -46,6 +64,18 @@ and get an **AI Skill that actually works like them**
| Markdown | ✅ | ✅ | — | Manual upload |
| Paste text directly | ✅ | — | — | Manual input |
### Recommended WeChat Chat Export Tools
These are independent open-source projects — this project does not include their code, but our parsers are compatible with their export formats. WeChat auto-decryption is currently unstable, so we recommend using these open-source tools to export chat history, then paste or import into this project:
| Tool | Platform | Description |
|------|----------|-------------|
| [WeChatMsg](https://github.com/LC044/WeChatMsg) | Windows | WeChat chat history export, supports multiple formats |
| [PyWxDump](https://github.com/xaoyaoo/PyWxDump) | Windows | WeChat database decryption & export |
| [留痕 (Liuhen)](https://github.com/greyovo/留痕) | macOS | WeChat chat history export (recommended for Mac users) |
> Tool recommendations from [@therealXiaomanChu](https://github.com/therealXiaomanChu). Thanks to all the open-source authors — together for cyber-immortality!
---
## Install
@@ -75,7 +105,7 @@ git clone https://github.com/titanwings/colleague-skill ~/.openclaw/workspace/sk
pip3 install -r requirements.txt
```
> Feishu/DingTalk auto-collection requires App credentials. See [INSTALL.md](INSTALL.md) for details.
> Feishu/DingTalk/Slack auto-collection requires App credentials. See [INSTALL.md](INSTALL.md) for details.
---
@@ -179,6 +209,7 @@ create-colleague/
│ ├── feishu_browser.py # Feishu browser method
│ ├── feishu_mcp_client.py # Feishu MCP method
│ ├── dingtalk_auto_collector.py # DingTalk auto-collector
│ ├── slack_auto_collector.py # Slack auto-collector
│ ├── email_parser.py # Email parser
│ ├── skill_writer.py # Skill file management
│ └── version_manager.py # Version archive & rollback

View File

@@ -63,7 +63,8 @@ def setup_config() -> None:
print(" im:chat.members:readonly 读取群成员")
print()
print(" 用户类:")
print(" contact:user.base:readonly 搜索用户")
print(" contact:user.base:readonly 读取用户基本信息")
print(" contact:department.base:readonly 遍历部门查找用户(按姓名搜索必需)")
print()
print(" 文档类:")
print(" docs:doc:readonly 读取文档")
@@ -133,36 +134,134 @@ def api_post(path: str, body: dict, config: dict) -> dict:
# ─── 用户搜索 ─────────────────────────────────────────────────────────────────
def find_user(name: str, config: dict) -> Optional[dict]:
"""通过姓名搜索飞书用户"""
print(f" 搜索用户:{name} ...", file=sys.stderr)
def _find_user_by_contact(name: str, config: dict) -> Optional[dict]:
"""通过邮箱或手机号查找用户(使用 tenant_access_token"""
# 判断输入类型
emails, mobiles = [], []
if "@" in name:
emails = [name]
elif name.replace("+", "").replace("-", "").isdigit():
mobiles = [name]
else:
return None # 不是邮箱或手机号,跳过
data = api_get(
"/search/v1/user",
{"query": name, "page_size": 10},
config,
)
body = {}
if emails:
body["emails"] = emails
if mobiles:
body["mobiles"] = mobiles
data = api_post("/contact/v3/users/batch_get_id", body, config)
if data.get("code") != 0:
print(f" 搜索用户失败code={data.get('code')}{data.get('msg')}", file=sys.stderr)
print(f" 邮箱/手机号查找失败code={data.get('code')}{data.get('msg')}", file=sys.stderr)
return None
users = data.get("data", {}).get("results", [])
user_list = data.get("data", {}).get("user_list", [])
for item in user_list:
user_id = item.get("user_id")
if user_id:
# 获取用户详情
detail = api_get(f"/contact/v3/users/{user_id}", {"user_id_type": "user_id"}, config)
if detail.get("code") == 0:
user_data = detail.get("data", {}).get("user", {})
print(f" 找到用户:{user_data.get('name', user_id)}", file=sys.stderr)
return user_data
# 如果详情拉不到,返回基本信息
return {"user_id": user_id, "open_id": item.get("open_id", ""), "name": name}
return None
def _find_user_by_department(name: str, config: dict) -> Optional[dict]:
"""遍历部门查找用户(使用 tenant_access_token需要 contact:department.base:readonly"""
print(f" 通过部门遍历查找 {name} ...", file=sys.stderr)
# 递归获取所有部门 ID
dept_ids = ["0"] # 0 = 根部门
queue = ["0"]
while queue:
parent_id = queue.pop(0)
data = api_get(
f"/contact/v3/departments/{parent_id}/children",
{"page_size": 50, "fetch_child": False},
config,
)
if data.get("code") != 0:
if parent_id == "0":
print(f" 部门遍历失败code={data.get('code')}{data.get('msg')}", file=sys.stderr)
print(f" 请确认已开通 contact:department.base:readonly 权限", file=sys.stderr)
return None
continue
children = data.get("data", {}).get("items", [])
for child in children:
child_id = child.get("department_id", "")
if child_id:
dept_ids.append(child_id)
queue.append(child_id)
print(f"{len(dept_ids)} 个部门,搜索用户 ...", file=sys.stderr)
# 在每个部门中查找用户
matches = []
for dept_id in dept_ids:
page_token = None
while True:
params = {"department_id": dept_id, "page_size": 50}
if page_token:
params["page_token"] = page_token
data = api_get("/contact/v3/users/find_by_department", params, config)
if data.get("code") != 0:
break
users = data.get("data", {}).get("items", [])
for u in users:
uname = u.get("name", "")
en_name = u.get("en_name", "")
if name in uname or name in en_name or uname == name or en_name == name:
matches.append(u)
if not data.get("data", {}).get("has_more"):
break
page_token = data.get("data", {}).get("page_token")
if len(matches) >= 10:
break # 够了
return _select_user(matches, name)
def _select_user(users: list, name: str) -> Optional[dict]:
"""从候选列表中选择用户"""
if not users:
print(f" 未找到用户:{name}", file=sys.stderr)
return None
# 去重(按 user_id
seen = set()
deduped = []
for u in users:
uid = u.get("user_id", u.get("open_id", id(u)))
if uid not in seen:
seen.add(uid)
deduped.append(u)
users = deduped
if len(users) == 1:
u = users[0]
print(f" 找到用户:{u.get('name')}{u.get('department_path', [''])[0]}", file=sys.stderr)
dept_ids = u.get("department_ids", [])
print(f" 找到用户:{u.get('name')}(部门:{dept_ids[0] if dept_ids else ''}", file=sys.stderr)
return u
# 多个结果,让用户选择
print(f"\n 找到 {len(users)} 个结果,请选择:")
for i, u in enumerate(users):
dept = u.get("department_path", [""])
dept_str = dept[0] if dept else ""
print(f" [{i+1}] {u.get('name')} {dept_str} {u.get('user_id', '')}")
dept_ids = u.get("department_ids", [])
dept_str = dept_ids[0] if dept_ids else ""
en = u.get("en_name", "")
label = f"{u.get('name', '')} ({en})" if en else u.get("name", "")
print(f" [{i+1}] {label} dept={dept_str} uid={u.get('user_id', '')}")
choice = input("\n 选择编号(默认 1").strip() or "1"
try:
@@ -172,6 +271,35 @@ def find_user(name: str, config: dict) -> Optional[dict]:
return users[0]
def find_user(name: str, config: dict) -> Optional[dict]:
"""搜索飞书用户
策略:
1. 如果输入是邮箱/手机号 → 直接用 batch_get_id最快
2. 否则 → 遍历部门查找(需要 contact:department.base:readonly
3. 如果部门遍历也失败 → 提示用户改用邮箱/手机号
"""
print(f" 搜索用户:{name} ...", file=sys.stderr)
# 方法 1邮箱/手机号直接查找
user = _find_user_by_contact(name, config)
if user:
return user
# 方法 2部门遍历
user = _find_user_by_department(name, config)
if user:
return user
# 都失败
print(f"\n ❌ 未能找到用户 {name}", file=sys.stderr)
print(f" 建议:", file=sys.stderr)
print(f" 1. 确认已开通 contact:department.base:readonly 权限", file=sys.stderr)
print(f" 2. 改用邮箱搜索:--name user@company.com", file=sys.stderr)
print(f" 3. 改用手机号搜索:--name +8613800138000", file=sys.stderr)
return None
# ─── 消息记录 ─────────────────────────────────────────────────────────────────
def get_chats_with_user(user_open_id: str, config: dict) -> list: