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.
Files changed (56) hide show
  1. {flashid_auto-0.2.2/src/flashid_auto.egg-info → flashid_auto-0.2.3}/PKG-INFO +1 -1
  2. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/examples/linkedin-mcp-server.template.json +3 -3
  3. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-mcp.md +3 -3
  4. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-people-search.md +1 -1
  5. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/pyproject.toml +1 -1
  6. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_people_search.py +52 -5
  7. {flashid_auto-0.2.2 → flashid_auto-0.2.3/src/flashid_auto.egg-info}/PKG-INFO +1 -1
  8. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_people_search.py +25 -3
  9. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/.env.example +0 -0
  10. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/LICENSE +0 -0
  11. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/MANIFEST.in +0 -0
  12. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/examples/linkedin-mcp-client.json +0 -0
  13. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/flashid-api-reference.md +0 -0
  14. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-mcp-release.md +0 -0
  15. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/linkedin-sales-navigator.md +0 -0
  16. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/docs/package-readme.md +0 -0
  17. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/requirements-dev.lock +0 -0
  18. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/requirements-mcp.lock +0 -0
  19. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/setup.cfg +0 -0
  20. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/__init__.py +0 -0
  21. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/__init__.py +0 -0
  22. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/base.py +0 -0
  23. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/contracts.py +0 -0
  24. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/environment.py +0 -0
  25. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/plan.py +0 -0
  26. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/registry.py +0 -0
  27. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/runner.py +0 -0
  28. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/automation/worker.py +0 -0
  29. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/cli.py +0 -0
  30. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/__init__.py +0 -0
  31. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/__main__.py +0 -0
  32. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/client.py +0 -0
  33. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/config.py +0 -0
  34. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/flashid/errors.py +0 -0
  35. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/__init__.py +0 -0
  36. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/__main__.py +0 -0
  37. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/server.py +0 -0
  38. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/service.py +0 -0
  39. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/mcp/tools.py +0 -0
  40. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/__init__.py +0 -0
  41. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/_linkedin.py +0 -0
  42. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/duckduckgo.py +0 -0
  43. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/modules/linkedin_sales_navigator.py +0 -0
  44. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto/tocsv.py +0 -0
  45. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/SOURCES.txt +0 -0
  46. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/dependency_links.txt +0 -0
  47. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/entry_points.txt +0 -0
  48. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/requires.txt +0 -0
  49. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/src/flashid_auto.egg-info/top_level.txt +0 -0
  50. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_automation.py +0 -0
  51. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_automation_worker.py +0 -0
  52. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_duckduckgo_browser.py +0 -0
  53. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_flashid_client.py +0 -0
  54. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_flashid_config_cli.py +0 -0
  55. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_mcp.py +0 -0
  56. {flashid_auto-0.2.2 → flashid_auto-0.2.3}/tests/test_linkedin_sales_navigator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashid-auto
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Modular automation for running FlashID browsers
5
5
  License-Expression: MIT
6
6
  Keywords: flashid,browser,automation,playwright
@@ -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.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.2",
16
+ "version": "0.2.3",
17
17
  "runtimeHint": "uvx",
18
18
  "runtimeArguments": [
19
- {"type":"named","name":"--with","value":"flashid-auto[mcp]==0.2.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.2。服务名和启动命令:`linkedin-mcp`。
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.2' linkedin-mcp --profile Navigator
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.2-py3-none-any.whl[mcp]' linkedin-mcp --profile Navigator
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 后等待异步加载的操作菜单按钮可见,再展开菜单、等待 “View LinkedIn profile” 链接可见并提取,最后关闭面板继续采集。面板框架先出现不代表员工资料已加载;菜单暂未出现时继续等待,达到配置超时仍不可用才回退。不会用 `page.goto()` 打开员工详情。缺失的可选字段为空字符串或空数组,按 lead 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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashid-auto"
7
- version = "0.2.2"
7
+ version = "0.2.3"
8
8
  description = "Modular automation for running FlashID browsers"
9
9
  readme = "docs/package-readme.md"
10
10
  license = "MIT"
@@ -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 actual action control instead of treating its absence as final.
194
- await trigger.wait_for(state="visible", timeout=optional_timeout)
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 link.first.wait_for(timeout=optional_timeout)
198
- return profile_url(await link.first.get_attribute("href")) or lead_url
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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashid-auto
3
- Version: 0.2.2
3
+ Version: 0.2.3
4
4
  Summary: Modular automation for running FlashID browsers
5
5
  License-Expression: MIT
6
6
  Keywords: flashid,browser,automation,playwright
@@ -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, ambiguousCompany=AMBIGUOUS_COMPANY_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
- outlet.querySelector('[aria-label="Open actions overflow menu"]').onclick=()=>
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