flashid-auto 0.2.2__tar.gz → 0.2.3__tar.gz
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.
- {flashid_auto-0.2.2/src/flashid_auto.egg-info → flashid_auto-0.2.3}/PKG-INFO +1 -1
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/examples/linkedin-mcp-server.template.json +3 -3
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-mcp.md +3 -3
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-people-search.md +1 -1
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/pyproject.toml +1 -1
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_people_search.py +52 -5
- {flashid_auto-0.2.2 → flashid_auto-0.2.3/src/flashid_auto.egg-info}/PKG-INFO +1 -1
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_people_search.py +25 -3
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/.env.example +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/LICENSE +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/MANIFEST.in +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/examples/linkedin-mcp-client.json +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/flashid-api-reference.md +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-mcp-release.md +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-sales-navigator.md +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/package-readme.md +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/requirements-dev.lock +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/requirements-mcp.lock +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/setup.cfg +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/__init__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/__init__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/base.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/contracts.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/environment.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/plan.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/registry.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/runner.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/worker.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/cli.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/__init__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/__main__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/client.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/config.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/errors.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/__init__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/__main__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/server.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/service.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/tools.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/__init__.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/_linkedin.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/duckduckgo.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_sales_navigator.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/tocsv.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/SOURCES.txt +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/dependency_links.txt +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/entry_points.txt +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/requires.txt +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/top_level.txt +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_automation.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_automation_worker.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_duckduckgo_browser.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_flashid_client.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_flashid_config_cli.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_mcp.py +0 -0
- {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_sales_navigator.py +0 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.replace-owner/linkedin-mcp",
|
|
4
4
|
"title": "LinkedIn MCP for FlashID",
|
|
5
5
|
"description": "Search LinkedIn Sales Navigator companies and employees in your own FlashID browser.",
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.3",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/replace-owner/replace-repository",
|
|
9
9
|
"source": "github"
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"registryBaseUrl": "https://pypi.org",
|
|
15
15
|
"identifier": "flashid-auto",
|
|
16
|
-
"version": "0.2.
|
|
16
|
+
"version": "0.2.3",
|
|
17
17
|
"runtimeHint": "uvx",
|
|
18
18
|
"runtimeArguments": [
|
|
19
|
-
{"type":"named","name":"--with","value":"flashid-auto[mcp]==0.2.
|
|
19
|
+
{"type":"named","name":"--with","value":"flashid-auto[mcp]==0.2.3"}
|
|
20
20
|
],
|
|
21
21
|
"packageArguments": [{"type":"positional","value":"mcp"}],
|
|
22
22
|
"transport": {"type":"stdio"},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# LinkedIn MCP 使用与接口契约
|
|
2
2
|
|
|
3
|
-
日期:2026-10-23。发行版本:flashid-auto 0.2.
|
|
3
|
+
日期:2026-10-23。发行版本:flashid-auto 0.2.3。服务名和启动命令:`linkedin-mcp`。
|
|
4
4
|
公司模块复用 `linkedin_sales_navigator` 1.2.0,人员模块复用 `linkedin_people_search` 2.3.0。
|
|
5
5
|
|
|
6
6
|
## 1. 目标与安装
|
|
@@ -46,7 +46,7 @@ Windows 使用 `.venv\Scripts\python.exe` 和 `.venv\Scripts\linkedin-mcp.exe`
|
|
|
46
46
|
目前发行包仍为本地构建,尚未公开上传。**
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
codex mcp add linkedin-mcp --env FLASHID_API_KEY=YOUR_FLASHID_API_KEY -- uvx --from 'flashid-auto[mcp]==0.2.
|
|
49
|
+
codex mcp add linkedin-mcp --env FLASHID_API_KEY=YOUR_FLASHID_API_KEY -- uvx --from 'flashid-auto[mcp]==0.2.3' linkedin-mcp --profile Navigator
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
将 `YOUR_FLASHID_API_KEY` 替换为用户自己的 Key。`codex mcp add` 保存 MCP 启动配置;
|
|
@@ -62,7 +62,7 @@ Codex 首次连接时,`uvx` 自动下载依赖、缓存独立环境并启动
|
|
|
62
62
|
发布前可以直接用本地 wheel 验证:
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
codex mcp add linkedin-mcp --env FLASHID_API_KEY=YOUR_FLASHID_API_KEY -- uvx --from '/absolute/path/to/flashid_auto-0.2.
|
|
65
|
+
codex mcp add linkedin-mcp --env FLASHID_API_KEY=YOUR_FLASHID_API_KEY -- uvx --from '/absolute/path/to/flashid_auto-0.2.3-py3-none-any.whl[mcp]' linkedin-mcp --profile Navigator
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
命令中的 wheel 路径须替换为本机实际绝对路径。该方式不依赖 PyPI 上尚未发布的本项目。
|
|
@@ -120,7 +120,7 @@ JSON 链见 [linkedin-company-people-search.json](examples/linkedin-company-peop
|
|
|
120
120
|
|
|
121
121
|
单次输出为 `{"data":{},"records":[],"artifacts":[]}`。`data` 包含 `action`、`search_key`、`company_name`、`company_id`、`requested_filters`、`applied_filters`、`requested_count`、`collected_count`、`pages_visited`、`complete`、`search_url`、`result_count_text`。搜索还含 `require_result_count`、`result_count_met`,终止时含 `stop_reason`;检查时含 `filters`。
|
|
122
122
|
|
|
123
|
-
每条人员记录包含 `rank`、`lead_id`、`name`、`url`、`title`、`company_name`、`company_url`、`location`、`tenure_text`、`about`、`connection_degree`、`recent_activity_text`、`badges`。`url` 优先使用 `https://www.linkedin.com/in/{slug}`;若详情面板无法提供有效公开链接,则使用 `https://www.linkedin.com/sales/lead/{id}`。回退链接只保留 lead ID,不带逗号后的路由附加信息、查询参数或片段;已在 `Navigator` 验证纯 ID 地址可正常打开。模块从列表卡片读取资料,直接点击员工姓名打开同一搜索页上的侧边详情面板,核对员工 ID
|
|
123
|
+
每条人员记录包含 `rank`、`lead_id`、`name`、`url`、`title`、`company_name`、`company_url`、`location`、`tenure_text`、`about`、`connection_degree`、`recent_activity_text`、`badges`。`url` 优先使用 `https://www.linkedin.com/in/{slug}`;若详情面板无法提供有效公开链接,则使用 `https://www.linkedin.com/sales/lead/{id}`。回退链接只保留 lead ID,不带逗号后的路由附加信息、查询参数或片段;已在 `Navigator` 验证纯 ID 地址可正常打开。模块从列表卡片读取资料,直接点击员工姓名打开同一搜索页上的侧边详情面板,核对员工 ID 后等待异步加载的操作菜单或 “Unlock full profile” 状态:前者出现时展开菜单、等待 “View LinkedIn profile” 链接并提取;后者出现时立即使用 `/sales/lead/` 回退链接,不再等待公开链接。两者均未出现时达到配置超时后回退。最后关闭面板继续采集。不会用 `page.goto()` 打开员工详情。缺失的可选字段为空字符串或空数组,按 lead ID 去重。
|
|
124
124
|
|
|
125
125
|
失败沿用框架 `MODULE_FAILED`,细分 `error.details.module_code`:`FILTER_UNAVAILABLE`、`FILTER_OPTION_NOT_FOUND`、`FILTER_NOT_APPLIED`、`PAGE_UNSUPPORTED`、`PAGINATION_STALLED`、`INSUFFICIENT_RESULTS`、`LOGIN_REQUIRED`、`SEARCH_CHALLENGE`、`SEARCH_TIMEOUT`、`SEARCH_FAILED`。Current company 无候选,或提供的公司 ID 无法精确匹配时返回 `FILTER_OPTION_NOT_FOUND`;`require_result_count=false` 仅允许人员结果正常耗尽时不足目标数,不会忽略公司筛选失败。公开链接缺失或无效,以及详情面板打开或菜单操作失败时,回退到 `/sales/lead/`,继续搜索。其他失败保留已解析人员的部分输出但 `complete=false`。参数与绑定预检错误为 `INVALID_PARAMS`。
|
|
126
126
|
|
{flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_people_search.py
RENAMED
|
@@ -188,14 +188,61 @@ class LeadSearchPage(SalesPage):
|
|
|
188
188
|
header_id = re.match(r"^/sales/lead/([^,/]+)", header_path)
|
|
189
189
|
if not header_id or header_id[1] != lead_url.rsplit("/", 1)[1]:
|
|
190
190
|
return lead_url
|
|
191
|
-
trigger = panel.get_by_role("button", name="Open actions overflow menu").first
|
|
192
191
|
# The panel header renders before the asynchronously loaded lead details.
|
|
193
|
-
# Wait for the
|
|
194
|
-
await
|
|
192
|
+
# Wait for either the public-profile action or the account-gated unlock state.
|
|
193
|
+
state = await self.page.wait_for_function(
|
|
194
|
+
r"""selector => {
|
|
195
|
+
const root = document.querySelector(selector);
|
|
196
|
+
if (!root) return false;
|
|
197
|
+
const visible = element => {
|
|
198
|
+
const style = window.getComputedStyle(element);
|
|
199
|
+
return style.display !== "none" &&
|
|
200
|
+
style.visibility !== "hidden" &&
|
|
201
|
+
!!(element.offsetWidth || element.offsetHeight ||
|
|
202
|
+
element.getClientRects().length);
|
|
203
|
+
};
|
|
204
|
+
const controls = root.querySelectorAll("button, a, [role='button']");
|
|
205
|
+
const unlock = Array.from(controls).some(element =>
|
|
206
|
+
element.textContent.trim() === "Unlock full profile" && visible(element));
|
|
207
|
+
const actions = Array.from(root.querySelectorAll(
|
|
208
|
+
"[aria-label='Open actions overflow menu']"
|
|
209
|
+
)).some(visible);
|
|
210
|
+
if (unlock) return "unlock";
|
|
211
|
+
return actions ? "actions" : false;
|
|
212
|
+
}""",
|
|
213
|
+
arg="#inline-sidesheet-outlet",
|
|
214
|
+
timeout=optional_timeout,
|
|
215
|
+
)
|
|
216
|
+
if await state.json_value() == "unlock":
|
|
217
|
+
await state.dispose()
|
|
218
|
+
return lead_url
|
|
219
|
+
await state.dispose()
|
|
220
|
+
trigger = None
|
|
221
|
+
triggers = panel.get_by_role("button", name="Open actions overflow menu", exact=True)
|
|
222
|
+
for index in range(await triggers.count()):
|
|
223
|
+
candidate = triggers.nth(index)
|
|
224
|
+
if await candidate.is_visible():
|
|
225
|
+
trigger = candidate
|
|
226
|
+
break
|
|
227
|
+
if trigger is None:
|
|
228
|
+
return lead_url
|
|
195
229
|
await trigger.click(timeout=optional_timeout)
|
|
196
230
|
link = self.page.get_by_role("link", name="View LinkedIn profile", exact=True)
|
|
197
|
-
await
|
|
198
|
-
|
|
231
|
+
await self.page.wait_for_function(
|
|
232
|
+
r"""() => Array.from(document.querySelectorAll("a")).some(element => {
|
|
233
|
+
const style = window.getComputedStyle(element);
|
|
234
|
+
return element.textContent.trim() === "View LinkedIn profile" &&
|
|
235
|
+
style.display !== "none" && style.visibility !== "hidden" &&
|
|
236
|
+
!!(element.offsetWidth || element.offsetHeight ||
|
|
237
|
+
element.getClientRects().length);
|
|
238
|
+
})""",
|
|
239
|
+
timeout=optional_timeout,
|
|
240
|
+
)
|
|
241
|
+
for index in range(await link.count()):
|
|
242
|
+
candidate = link.nth(index)
|
|
243
|
+
if await candidate.is_visible():
|
|
244
|
+
return profile_url(await candidate.get_attribute("href")) or lead_url
|
|
245
|
+
return lead_url
|
|
199
246
|
except (PlaywrightError, ValueError):
|
|
200
247
|
return lead_url
|
|
201
248
|
finally:
|
|
@@ -116,6 +116,7 @@ def people_html(
|
|
|
116
116
|
profile_href=None,
|
|
117
117
|
no_detail_panel=False,
|
|
118
118
|
locked_profile=False,
|
|
119
|
+
unlock_full_profile=False,
|
|
119
120
|
ambiguous_company=False,
|
|
120
121
|
detail_delay_ms=0,
|
|
121
122
|
menu_delay_ms=50,
|
|
@@ -157,7 +158,8 @@ def people_html(
|
|
|
157
158
|
<script>
|
|
158
159
|
const total=TOTAL, broken=BROKEN, missingCompany=MISSING_COMPANY;
|
|
159
160
|
const profileHref=PROFILE_HREF_VALUE, noDetailPanel=NO_DETAIL_PANEL_VALUE;
|
|
160
|
-
const lockedProfile=LOCKED_PROFILE_VALUE,
|
|
161
|
+
const lockedProfile=LOCKED_PROFILE_VALUE, unlockFullProfile=UNLOCK_FULL_PROFILE_VALUE;
|
|
162
|
+
const ambiguousCompany=AMBIGUOUS_COMPANY_VALUE;
|
|
161
163
|
const detailDelay=DETAIL_DELAY_VALUE, menuDelay=MENU_DELAY_VALUE;
|
|
162
164
|
const noCompanyCandidates=NO_COMPANY_CANDIDATES_VALUE;
|
|
163
165
|
let batch=0, revision=0;
|
|
@@ -239,11 +241,17 @@ def people_html(
|
|
|
239
241
|
outlet.querySelector('[aria-label="Dismiss"]').onclick=()=>outlet.innerHTML='';
|
|
240
242
|
setTimeout(()=>{
|
|
241
243
|
if(!header.isConnected)return;
|
|
244
|
+
if(unlockFullProfile){
|
|
245
|
+
outlet.insertAdjacentHTML('beforeend','<button>Unlock full profile</button>');
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
242
248
|
if(lockedProfile)return;
|
|
243
249
|
outlet.insertAdjacentHTML('beforeend',
|
|
250
|
+
'<button hidden aria-label="Open actions overflow menu">Actions</button>'+
|
|
244
251
|
'<button aria-label="Open actions overflow menu">Actions</button>'+
|
|
245
|
-
(href?'<a hidden href="'+href+'">View LinkedIn profile</a>'
|
|
246
|
-
|
|
252
|
+
(href?'<a hidden href="'+href+'">View LinkedIn profile</a>'+
|
|
253
|
+
'<a href="'+href+'">View LinkedIn profile</a>':''));
|
|
254
|
+
outlet.querySelectorAll('[aria-label="Open actions overflow menu"]')[1].onclick=()=>
|
|
247
255
|
setTimeout(()=>{for(const a of outlet.querySelectorAll('a'))a.hidden=false},menuDelay);
|
|
248
256
|
},detailDelay);
|
|
249
257
|
}
|
|
@@ -269,6 +277,7 @@ def people_html(
|
|
|
269
277
|
.replace("PROFILE_HREF_VALUE", json.dumps(profile_href))
|
|
270
278
|
.replace("NO_DETAIL_PANEL_VALUE", "true" if no_detail_panel else "false")
|
|
271
279
|
.replace("LOCKED_PROFILE_VALUE", "true" if locked_profile else "false")
|
|
280
|
+
.replace("UNLOCK_FULL_PROFILE_VALUE", "true" if unlock_full_profile else "false")
|
|
272
281
|
.replace("AMBIGUOUS_COMPANY_VALUE", json.dumps(ambiguous_company))
|
|
273
282
|
.replace("DETAIL_DELAY_VALUE", str(detail_delay_ms))
|
|
274
283
|
.replace("MENU_DELAY_VALUE", str(menu_delay_ms))
|
|
@@ -544,6 +553,19 @@ def test_locked_profile_falls_back_after_configured_timeout():
|
|
|
544
553
|
assert time.monotonic() - started < 5
|
|
545
554
|
|
|
546
555
|
|
|
556
|
+
@pytest.mark.browser
|
|
557
|
+
def test_unlock_full_profile_skips_public_profile_wait():
|
|
558
|
+
started = time.monotonic()
|
|
559
|
+
result = asyncio.run(
|
|
560
|
+
execute(
|
|
561
|
+
{"company_name": "Qualitrol", "result_count": 1, "timeout_ms": 5000},
|
|
562
|
+
html=people_html(total=1, unlock_full_profile=True, detail_delay_ms=100),
|
|
563
|
+
)
|
|
564
|
+
)
|
|
565
|
+
assert result["records"][0]["url"] == "https://www.linkedin.com/sales/lead/1"
|
|
566
|
+
assert time.monotonic() - started < 2
|
|
567
|
+
|
|
568
|
+
|
|
547
569
|
@pytest.mark.browser
|
|
548
570
|
@pytest.mark.parametrize("detail_delay,menu_delay", [(500, 50), (3300, 50), (50, 3300)])
|
|
549
571
|
def test_public_profile_waits_for_delayed_details_and_menu(detail_delay, menu_delay):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_sales_navigator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|