Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ebc785d1fe | |||
| ba932283f2 | |||
| f05bf68f61 | |||
| 35ab12a20a | |||
| fd7c54a695 | |||
| eafd834daf | |||
| f575ae0fa4 | |||
| 797c9f8575 | |||
| fb3ac4d454 | |||
| c629d706ec | |||
| 7896786311 | |||
| 820a2654ff | |||
| 8d45ab8436 | |||
| 855020636b | |||
| ff82603839 | |||
| 90270fb76c | |||
| c9edeaea44 | |||
| 757fe07b4d | |||
| f04fa706f7 | |||
| 9daf914a89 | |||
| c8e5ff3010 | |||
| f5591dd3ea | |||
| 62a0d988ce | |||
| dc41969503 | |||
| 9d9a4b9698 | |||
| b171bf210f | |||
| c39678dece | |||
| 6c1f552606 | |||
| dcad67d4b4 | |||
| 15f3af406c | |||
| b50abcc4ec | |||
| 20f1b1f51e | |||
| cd035cd30f | |||
| a7cc935453 | |||
| 349609614b | |||
| ad680cee99 | |||
| e95b2d6a28 | |||
| f55c1b984b | |||
| 5ab34a2f50 | |||
| de24d97a22 | |||
| 317b2a1f10 | |||
| 7bc2fa4d59 | |||
| 9d5c5a8969 | |||
| 052d302f16 | |||
| bb4da7fd86 | |||
| 359e6eadc8 | |||
| e41a2609b3 | |||
| b23c909953 | |||
| 79bf9e2aee | |||
| d9d8d558a9 | |||
| 6afd3dcbdf | |||
| 5809fe3249 | |||
| e626e0b8ce | |||
| 284098ff0d | |||
| 90e1c297f4 | |||
| 23adbea744 | |||
| 9af3a3f73e | |||
| cc27ff9305 | |||
| 8a75c15557 | |||
| e9eaf44bdd | |||
| 2ad2d06d01 | |||
| 8bf7abfe76 | |||
| 099a1251cd | |||
| 354397a1b4 | |||
| a1d9e11c05 | |||
| 33538cca05 | |||
| 50f4866142 | |||
| 28a2ebb817 | |||
| 7a08022c72 | |||
| 78c829d955 | |||
| 5008345ce8 | |||
| a1b7684bf2 | |||
| da67f3b07e | |||
| c72ff95670 | |||
| d7a38abdbd | |||
| 6fed95b365 | |||
| 0099c1ca8b | |||
| c5c7e8af55 | |||
| 83476da97f | |||
| 4b6b591849 | |||
| b5ec9c4406 | |||
| ca197d14e9 | |||
| acfb336a9a | |||
| 4d27d49752 | |||
| b46693f525 | |||
| c3720ce6b3 | |||
| f68c73c5e9 | |||
| 58d992d875 | |||
| e9784a16d0 | |||
| ebe3a3f7c3 | |||
| 9ebb629aca | |||
| 4f80ec1459 | |||
| 9104064e8c | |||
| a43e653a74 | |||
| 3bffd76626 | |||
| 015101d6ae | |||
| 8904e602dd | |||
| d384967e41 | |||
| 531b59f538 | |||
| bef26c3feb | |||
| c48dd538a0 | |||
| 950767c1bf | |||
| 12b6001d53 | |||
| 2c1972ed89 | |||
| 362bf822f7 | |||
| 4734e2df26 | |||
| c2648c1749 | |||
| 7071a6c26a | |||
| 303ece84fd | |||
| 8a0266ce8d | |||
| cb55daf47d | |||
| 43415d3139 | |||
| 9cc9edf17e | |||
| e290e2f67a | |||
| 4272939217 | |||
| 148bdb6839 | |||
| 68cee4cc64 | |||
| 7e9027167d | |||
| 6630774dc9 | |||
| 0ca1c8de0b | |||
| 85a33ac1a4 | |||
| b8fd1aaebc | |||
| 13d952dc9d | |||
| 1f2f5a44d3 | |||
| 8159aa58a6 | |||
| 2bdb3be81c | |||
| bde630f55d | |||
| 3efef0ec42 | |||
| 1141287733 | |||
| ec08b05da1 | |||
| 5ea2edd13c | |||
| 1999bbeda9 | |||
| b3ec571341 | |||
| d0de8ad838 | |||
| 0402a6be03 | |||
| f2b6c9dfd2 | |||
| 005bc8ab44 |
@@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
name: git-cleanup
|
||||||
|
description: Clean up local git branches and remotes accumulated from PR reviews. Use when the user asks to clean branches, remove stale remotes, or tidy up the local git state.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Git Cleanup
|
||||||
|
|
||||||
|
Clean local branches (except `main` and current branch) and non-origin remotes.
|
||||||
|
Useful after reviewing multiple PRs that leave behind tracking branches and contributor remotes.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Survey
|
||||||
|
|
||||||
|
Run these in parallel:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch
|
||||||
|
git remote
|
||||||
|
git status --short
|
||||||
|
git stash list
|
||||||
|
```
|
||||||
|
|
||||||
|
Present a summary of how many branches and remotes will be removed.
|
||||||
|
|
||||||
|
### 2. Safety Checks — Ask Before Proceeding
|
||||||
|
|
||||||
|
**Stop and ask for confirmation if any of these are true:**
|
||||||
|
|
||||||
|
- Current branch is NOT `main`
|
||||||
|
- Working tree has uncommitted changes or stashes
|
||||||
|
- Any branch is ahead of its upstream
|
||||||
|
|
||||||
|
List unmerged branches separately and let the user decide.
|
||||||
|
|
||||||
|
### 3. Delete Branches
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Safe delete first (fails on unmerged branches)
|
||||||
|
git branch | grep -v '^\*' | grep -v '^\s*main$' | xargs git branch -d 2>&1
|
||||||
|
|
||||||
|
# Force-delete only with explicit user approval
|
||||||
|
git branch -D <branch>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Remove Remotes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote | grep -v '^origin$' | xargs -I{} git remote remove {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Confirm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git branch && echo "---" && git remote
|
||||||
|
```
|
||||||
|
|
||||||
|
Report what was cleaned up.
|
||||||
|
|
||||||
|
## Key Rules
|
||||||
|
|
||||||
|
- **NEVER** delete `main` or the current checked-out branch
|
||||||
|
- **NEVER** force-delete unmerged branches without user confirmation
|
||||||
|
- If in doubt, ask
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
name: pre-impl-discussion
|
||||||
|
description: "Conduct a thorough pre-implementation discussion before making significant changes. Use when the user wants to discuss, plan, or evaluate a change before implementing it — especially when they say words like 'discuss', 'evaluate', 'plan', or 'let's talk about'."
|
||||||
|
argument-hint: 'Describe the change to evaluate'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pre-Implementation Discussion
|
||||||
|
|
||||||
|
Facilitate a structured, collaborative discussion to evaluate a proposed change **before any implementation begins**.
|
||||||
|
|
||||||
|
## Golden Rule
|
||||||
|
|
||||||
|
**Do NOT implement anything until the user explicitly confirms the final plan and says to proceed.** Not even "let me try on a branch" — that's implementation. The user will tell you when discussion is over.
|
||||||
|
|
||||||
|
## Interaction Style
|
||||||
|
|
||||||
|
- **Concise during discussion.** Each intermediate response should be short and focused on the current question. Do NOT repeat the full plan in every response.
|
||||||
|
- **Complete when finalizing.** Once the plan is mature and the user asks for it, present a single comprehensive (but terse) summary for final review.
|
||||||
|
- **Ask, don't assume.** When uncertain about project context, constraints, or preferences — ask. The user prefers collaborative discussion over receiving a pre-baked answer.
|
||||||
|
- **Challenge the premise.** Question whether the proposed change is the right one. Suggest simpler alternatives if they exist.
|
||||||
|
- **Match the user's language.** Reply in the same language the user writes in.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Understand the Current State
|
||||||
|
|
||||||
|
Before forming any opinion:
|
||||||
|
|
||||||
|
- **Read the relevant source files** — configs, code, docs that relate to the change
|
||||||
|
- **Understand the project structure** — what's published vs private, what environments things target, existing constraints
|
||||||
|
- **Map the impact surface** — which files, packages, or systems would be affected
|
||||||
|
- **Estimate implementation cost** — how many files to touch, how much config to rewrite, what could break
|
||||||
|
|
||||||
|
Do NOT skip this step. Do NOT rely on assumptions about what "most projects" do.
|
||||||
|
|
||||||
|
### 2. Research Thoroughly
|
||||||
|
|
||||||
|
- **Consult official sources** — docs, changelogs, migration guides, GitHub issues
|
||||||
|
- **Verify every claim with evidence.** Don't say "X supports Y" without a source. If unsure, say so.
|
||||||
|
- **Search in parallel** to save time — batch independent queries
|
||||||
|
|
||||||
|
Common pitfalls:
|
||||||
|
|
||||||
|
- Assuming compatibility without checking actual version constraints
|
||||||
|
- Confusing roadmap/aspirations with actual released state
|
||||||
|
- Missing transitive constraints (a dependency of a dependency)
|
||||||
|
|
||||||
|
### 3. Present Findings (Concise)
|
||||||
|
|
||||||
|
Share a **brief** assessment. Tables work well for comparisons. Highlight **blockers** and **unknowns** prominently — don't bury them in paragraphs.
|
||||||
|
|
||||||
|
### 4. Identify Key Decisions
|
||||||
|
|
||||||
|
Surface the decisions the user needs to make. Present them as clear choices with trade-offs, not as a recommendation monologue.
|
||||||
|
|
||||||
|
For each decision point:
|
||||||
|
|
||||||
|
- What are the options? (2-3 max)
|
||||||
|
- What does each option cost or give up?
|
||||||
|
- What's your lean and why? (one sentence)
|
||||||
|
|
||||||
|
### 5. Iterate
|
||||||
|
|
||||||
|
The user will ask follow-up questions, raise concerns, or challenge assumptions. For each round:
|
||||||
|
|
||||||
|
- **Research if needed** — don't answer from instinct when facts are available
|
||||||
|
- **Answer only what was asked** — don't re-present the entire plan
|
||||||
|
- **Update your mental model** based on user feedback
|
||||||
|
|
||||||
|
Common mistakes in this phase:
|
||||||
|
|
||||||
|
- Repeating the full plan after every small clarification
|
||||||
|
- Answering a narrow question with a broad redesign
|
||||||
|
- Treating user questions as confirmation to proceed
|
||||||
|
|
||||||
|
### 6. Finalize the Plan
|
||||||
|
|
||||||
|
When the user signals the discussion is converging, present the **complete plan once**:
|
||||||
|
|
||||||
|
- **What changes** — concrete list of actions (table or bullet points)
|
||||||
|
- **Impact surface** — what files/systems are affected, estimated effort
|
||||||
|
- **What does NOT change** — explicitly state scope boundaries
|
||||||
|
- **Risks or open items** — anything that needs testing or further verification
|
||||||
|
|
||||||
|
Keep it terse. Tables over paragraphs. No explanations the user already heard during discussion.
|
||||||
|
|
||||||
|
### 7. Wait for Confirmation
|
||||||
|
|
||||||
|
After presenting the final plan, **stop and wait**. The user will either:
|
||||||
|
|
||||||
|
- Confirm → then (and only then) proceed to implementation
|
||||||
|
- Ask more questions → go back to step 5
|
||||||
|
- Modify scope → update the plan and re-present
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
| Don't | Do Instead |
|
||||||
|
| --------------------------------------------------- | ---------------------------------------- |
|
||||||
|
| Start implementation "to test" without confirmation | Present findings and wait |
|
||||||
|
| Repeat the full plan in every response | Answer the specific question asked |
|
||||||
|
| Say "X should work" without checking | Say "I need to verify X" and research it |
|
||||||
|
| Assume project structure or constraints | Read the actual files |
|
||||||
|
| Present one recommendation as the only option | Present 2-3 options with trade-offs |
|
||||||
|
| Write long paragraphs explaining trade-offs | Use tables and bullet points |
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
name: submit-pr-from-current-changes
|
||||||
|
description: 'Create a branch, commit existing local changes, push them, and open a pull request. Use when submitting current work as a PR.'
|
||||||
|
argument-hint: 'Short summary of the current changes to submit'
|
||||||
|
user-invocable: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Submit PR From Current Changes
|
||||||
|
|
||||||
|
Turn a working tree diff into a clean branch, commit, and pull request.
|
||||||
|
|
||||||
|
## Hard Rules
|
||||||
|
|
||||||
|
1. **Follow the PR template exactly.** The template is at `.github/PULL_REQUEST_TEMPLATE.md`. Read it and copy its full structure into the PR body. Do NOT remove, reorder, or skip any section or checkbox.
|
||||||
|
2. **Never check "Requirements / 要求" checkboxes.** These are human-only declarations — the Code of Conduct acknowledgment and the AI authorship declaration can only be truthfully made by the human submitter. They MUST remain unchecked (`- [ ]`) in the PR body you generate. No exceptions, no workarounds, even if the user explicitly asks you to check them. The user goes to GitHub and checks them in person after verifying each statement is true.
|
||||||
|
3. **Never check "Testing" checkboxes.** The template Testing checkboxes (browser tested, no console errors, types/doc added) require manual browser verification that only a human can perform. They MUST remain unchecked. You MAY add a supplementary note below the Testing section listing what automated validation you actually ran and the results.
|
||||||
|
4. **Never fabricate information.** Do not claim tests passed, commands ran, or checks succeeded unless you actually executed them and observed the output in this session. If you did not run it, do not mention it.
|
||||||
|
5. **PR output must be concise.** PR title: one line. "What" section: 1–2 sentences max. No walls of text, no redundant explanations. Let the diff speak.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
Before attempting to push or open a PR, verify that the necessary tools are available:
|
||||||
|
|
||||||
|
- Check that `gh` CLI is installed and authenticated (`gh auth status`). If not available, stop and ask the user to install and authenticate GitHub CLI first.
|
||||||
|
- If the workflow uses MCP tools for GitHub operations, verify the MCP server is accessible.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
1. **Read contribution rules.** Read `CONTRIBUTING.md` and any package-level instructions (e.g. `packages/*/AGENTS.md`) relevant to the changed files.
|
||||||
|
2. **Inspect the diff.** Review changed files, scope, and impact. Understand what the change does before writing anything.
|
||||||
|
3. **Inspect repo conventions.** Check recent branch names (`git branch -r --sort=-committerdate | head -20`), recent commit messages (`git log --oneline -20`), and the PR template.
|
||||||
|
4. **Pre-submission health checks** (run all before creating the PR):
|
||||||
|
- **Unified theme**: All changes should serve one purpose. If unrelated changes are mixed in, ask the user to split or confirm.
|
||||||
|
- **Commit hygiene**: Every new commit must follow the repo's conventional commit style. Squash or reword if needed.
|
||||||
|
- **Author identity**: Verify `git config user.name` and `git config user.email` are set and the email looks real (not empty, not `noreply` unless intentional). Warn the user if not.
|
||||||
|
- **No leftover artifacts**: Check for debug logs, `.only` in tests, conflict markers, or temp files in the diff.
|
||||||
|
- **Lint and build**: Run lint/build for the affected area. Record results honestly.
|
||||||
|
5. **Warn if the PR looks too hasty.** If any of these are true, pause and warn the user before proceeding:
|
||||||
|
- Large diff with no description of intent provided
|
||||||
|
- Changes touch core lib or extension (where vibe coding is prohibited per `CONTRIBUTING.md`)
|
||||||
|
- Multiple unrelated concerns in one diff
|
||||||
|
- No validation has been run at all
|
||||||
|
- Remind the user: _"This project does not accept low-quality or AI-generated PRs without meaningful human review. Please review your changes carefully."_
|
||||||
|
6. **Branch.** Check the current branch first:
|
||||||
|
- If already on a non-main feature branch with a valid name (matching `type/topic` convention), reuse it.
|
||||||
|
- If the branch name does not follow repo conventions (e.g. missing prefix, unclear topic), ask the user whether to rename or create a new one.
|
||||||
|
- If on `main` or a default branch, create a new branch from it with a short kebab-case name: prefix (`fix/`, `feat/`, `docs/`, `refactor/`, `chore/`) + concrete topic words.
|
||||||
|
7. **Stage and commit.** Stage only the intended files. Use `type(scope): subject` format. Keep the subject specific and compact.
|
||||||
|
8. **Push.** Push with `-u origin HEAD`.
|
||||||
|
9. **Open PR.** Use `gh pr create` with the full PR template structure. Fill in "What" and "Type" sections based on the actual diff. Leave all "Testing" and "Requirements" checkboxes unchecked.
|
||||||
|
10. **Report results.** Tell the user: branch name, commit hash, PR link, and what validation actually ran (with pass/fail).
|
||||||
|
|
||||||
|
## Post-Submission Reminder
|
||||||
|
|
||||||
|
After successfully opening the PR, ALWAYS give a brief reminder in the user's language. Keep it concise and natural, but make sure it clearly tells the user:
|
||||||
|
|
||||||
|
1. They need to test the changes themselves in the browser.
|
||||||
|
2. They need to go to the PR page and check the Testing and Requirements checkboxes only after verifying each item.
|
||||||
|
3. The PR will not enter review until those checkboxes are checked.
|
||||||
|
4. The project does not accept autonomously AI-generated PRs, so they should only check the AI declaration if it is truthful.
|
||||||
|
|
||||||
|
## Branch Naming
|
||||||
|
|
||||||
|
- Prefer short kebab-case names with a repo-consistent prefix such as `fix/`, `feat/`, `docs/`, `refactor/`, or `chore/`.
|
||||||
|
- Match the change type first, then the smallest useful topic.
|
||||||
|
- Prefer concrete topic words over issue text dumps.
|
||||||
|
|
||||||
|
## Commit Style
|
||||||
|
|
||||||
|
- Use the repository's prevailing commit style (conventional commits).
|
||||||
|
- Use `type(scope): subject` when scope is clear from the changed area.
|
||||||
|
- Keep the subject specific and compact.
|
||||||
|
- If multiple commits exist on the branch, each one must independently follow conventions.
|
||||||
|
|
||||||
|
## Validation Strategy
|
||||||
|
|
||||||
|
- Default to enough validation to defend the PR, not the absolute minimum.
|
||||||
|
- Run `npm run ci` to run all build and lint checks.
|
||||||
|
- Escalate to broader validation when the diff crosses packages, changes shared code, or affects release behavior.
|
||||||
|
- Never claim checks that did not actually run.
|
||||||
|
- You MAY note what you ran and the results below the Testing section in the PR body, but do NOT check the template checkboxes.
|
||||||
|
|
||||||
|
## Decision Points
|
||||||
|
|
||||||
|
- No local changes → stop, say so.
|
||||||
|
- `gh` CLI not available → stop, ask user to install it.
|
||||||
|
- Unrelated files mixed → stage only the intended subset or ask whether the work should be split.
|
||||||
|
- If the repo has area-specific instructions, read them before naming the branch or writing the PR.
|
||||||
|
- Change is small but high-risk → prefer broader validation.
|
||||||
|
- Change is narrowly scoped and low-risk → run the most relevant checks, not arbitrary unrelated ones.
|
||||||
|
- Template has declarations you cannot truthfully check → leave unchecked, tell the user why.
|
||||||
|
|
||||||
|
## Completion Checks
|
||||||
|
|
||||||
|
- The branch exists remotely and tracks upstream.
|
||||||
|
- The commit message(s) match repo style.
|
||||||
|
- All commit authors have proper name and email configured.
|
||||||
|
- The PR title matches the commit intent.
|
||||||
|
- The PR body follows the template structure completely.
|
||||||
|
- All "Requirements" and "Testing" checkboxes are unchecked (`- [ ]`) in the PR body. Double-check: if any `- [x]` appears in these sections, it is a violation — fix it before submitting.
|
||||||
|
- The reported validation is accurate — nothing fabricated.
|
||||||
|
- The post-submission reminder was delivered in the user's language, concisely and accurately.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
name: update-changelog
|
||||||
|
description: 'Update docs/CHANGELOG.md from git history, GitHub releases, and code diffs. Use when: writing release notes, syncing the latest changelog entry, summarizing a new tag, or keeping changelog wording concise and consistent.'
|
||||||
|
argument-hint: 'Describe the target, for example: latest version only'
|
||||||
|
---
|
||||||
|
|
||||||
|
# Update Changelog
|
||||||
|
|
||||||
|
Update `docs/CHANGELOG.md` from repository evidence instead of guesswork.
|
||||||
|
|
||||||
|
## When to Use
|
||||||
|
|
||||||
|
- Add the newest release entry to `docs/CHANGELOG.md`
|
||||||
|
- Sync changelog text with GitHub Releases
|
||||||
|
- Summarize the latest tag from git history or code diffs
|
||||||
|
|
||||||
|
## Defaults
|
||||||
|
|
||||||
|
- Keep the wording brief
|
||||||
|
- Only add the latest missing version unless the user explicitly asks to backfill older releases
|
||||||
|
- Prefer GitHub sources first, especially Releases and tag metadata
|
||||||
|
- Skip `Features` / `Improvements` / `Bug Fixes` headings when the release only has a few clear items
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
### 1. Read the local style
|
||||||
|
|
||||||
|
- Open `docs/CHANGELOG.md`
|
||||||
|
- Match the existing tone, bullet style, section ordering, and date format
|
||||||
|
|
||||||
|
### 2. Determine the target release
|
||||||
|
|
||||||
|
- Read the root `package.json` version and compare it with the top changelog entry
|
||||||
|
- Find the previous tag for the target version
|
||||||
|
- If the latest version is already documented, stop and report that no changelog update is needed
|
||||||
|
|
||||||
|
### 3. Gather evidence
|
||||||
|
|
||||||
|
Prefer these sources in order:
|
||||||
|
|
||||||
|
1. GitHub release notes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GH_PAGER=cat gh release view v<version> --repo <owner>/<repo> --json tagName,name,publishedAt,body
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Tag date
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log -1 --format=%cs v<version>
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Commit history between tags
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git --no-pager log --format='%h %s' --no-merges v<previous>..v<version>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Diff scope when commit subjects are vague
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git --no-pager diff --name-only v<previous>..v<version>
|
||||||
|
git --no-pager diff --stat v<previous>..v<version>
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Read touched files directly only when the user-visible change is still unclear
|
||||||
|
|
||||||
|
### 4. Distill what belongs in the changelog
|
||||||
|
|
||||||
|
Include:
|
||||||
|
|
||||||
|
- User-visible features
|
||||||
|
- Important behavior changes
|
||||||
|
- Bug fixes that improve reliability, compatibility, or developer experience
|
||||||
|
- Small docs updates only when they materially change supported setups or onboarding
|
||||||
|
|
||||||
|
Exclude unless explicitly requested:
|
||||||
|
|
||||||
|
- Pure version bumps
|
||||||
|
- Routine dependency updates
|
||||||
|
- Internal refactors with no visible impact
|
||||||
|
- Mechanical formatting noise
|
||||||
|
|
||||||
|
### 5. Choose the structure
|
||||||
|
|
||||||
|
- If the release has 1 to 3 clear points, write flat bullets directly under the version heading
|
||||||
|
- If the release has several distinct items, use short headings such as `### Features`, `### Improvements`, and `### Bug Fixes`
|
||||||
|
- Do not force categories when they make the entry longer or noisier
|
||||||
|
|
||||||
|
### 6. Write the entry
|
||||||
|
|
||||||
|
- Add or update only the requested release entry
|
||||||
|
- Keep bullets short and concrete
|
||||||
|
- Reuse project terminology from nearby entries
|
||||||
|
- Avoid marketing language
|
||||||
|
- Do not copy noisy GitHub release text verbatim
|
||||||
|
|
||||||
|
### 7. Verify
|
||||||
|
|
||||||
|
- Check dates, version numbers, and tag boundaries
|
||||||
|
- Ensure Markdown structure matches nearby entries
|
||||||
|
- Confirm no intermediate versions were added unless requested
|
||||||
|
|
||||||
|
## Completion Checks
|
||||||
|
|
||||||
|
- The newest requested version is documented
|
||||||
|
- The wording is concise and consistent with the surrounding changelog
|
||||||
|
- The entry is backed by GitHub releases, git history, or code diff evidence
|
||||||
|
- Low-signal internal changes were left out
|
||||||
@@ -1,61 +1,59 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Report a bug
|
description: Report a bug
|
||||||
title: '[Bug] '
|
title: '[Bug] '
|
||||||
labels: ['bug']
|
type: Bug
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please search for existing issues.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢参与我们的社区!请先搜索现有 Issue,避免重复提交。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: What happened?
|
label: What happened? / 问题描述
|
||||||
placeholder: Describe the bug and expected behavior
|
description: Describe what you expected vs what actually happened.
|
||||||
|
placeholder: 'A clear description of the bug.'
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: code
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: Code
|
label: How to reproduce / 如何复现
|
||||||
render: typescript
|
description: Provide steps, URL, config or code snippet to reproduce the bug.
|
||||||
placeholder: Minimal reproduction code
|
placeholder: '1. Open URL: https://...'
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: version
|
||||||
|
attributes:
|
||||||
|
label: Version
|
||||||
|
placeholder: '0.0.0'
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: browser
|
id: browser
|
||||||
attributes:
|
attributes:
|
||||||
label: Browser
|
label: Browser
|
||||||
placeholder: 'Chrome 120, Firefox 119, etc.'
|
placeholder: 'e.g. Chrome 130'
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: version
|
|
||||||
placeholder: '0.0.0'
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: community
|
id: checklist
|
||||||
attributes:
|
attributes:
|
||||||
label: Community Communication / 社区沟通
|
label: Before submitting
|
||||||
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
|
|
||||||
options:
|
options:
|
||||||
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
||||||
required: true
|
required: true
|
||||||
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
|
|
||||||
required: true
|
|
||||||
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
- label: I have read the Code of Conduct. / 我已阅读行为准则。
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have searched existing issues and this is not a duplicate.
|
||||||
|
required: true
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Questions & Ideas / 问题与想法(Discussions)
|
|
||||||
url: https://github.com/alibaba/page-agent/discussions
|
|
||||||
about: Use Discussions for Q&A and ideation. 使用 Discussions 进行问答与想法交流。
|
|
||||||
- name: Security Report / 安全问题报告
|
|
||||||
url: https://github.com/alibaba/page-agent/security/policy
|
|
||||||
about: Report security vulnerabilities responsibly. 通过安全页面报告漏洞。
|
|
||||||
- name: Contributing Guide / 贡献指南
|
- name: Contributing Guide / 贡献指南
|
||||||
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
url: https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md
|
||||||
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
about: How to contribute code and ideas. 如何进行贡献与提交代码。
|
||||||
|
|||||||
@@ -1,32 +1,28 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a feature
|
description: Suggest a feature
|
||||||
title: '[Feature] '
|
title: '[Feature] '
|
||||||
labels: ['enhancement']
|
type: Feature
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Thanks for your interest in improving the project! Before submitting, please read our guidelines.
|
Thanks for your interest in improving the project! Before submitting, please search for existing issues.
|
||||||
感谢您对改进项目的兴趣!提交前请阅读我们的指南。
|
感谢参与我们的社区!请先搜索现有 Issue,避免重复提交。
|
||||||
|
|
||||||
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
- [Code of Conduct](https://github.com/alibaba/page-agent/blob/main/docs/CODE_OF_CONDUCT.md)
|
||||||
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
- [Contributing Guide](https://github.com/alibaba/page-agent/blob/main/CONTRIBUTING.md)
|
||||||
|
|
||||||
|
> 为了让更多人参与讨论,请尽量使用英文提交 Issue,感谢理解 🙏
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Feature Description / 功能描述
|
label: Feature Description / 功能描述
|
||||||
description: Describe the problem, solution, and any API changes. / 描述问题、解决方案以及 API 变更。
|
description: Describe the problem, solution, and any API changes.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
**Problem**:
|
**Problem**:
|
||||||
What problem does this solve?
|
|
||||||
|
|
||||||
**Solution**:
|
**Solution**:
|
||||||
How should this work?
|
|
||||||
|
|
||||||
**Proposed API**:
|
**Proposed API**:
|
||||||
```typescript
|
|
||||||
// code here
|
|
||||||
```
|
```
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -34,14 +30,13 @@ body:
|
|||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
id: community
|
id: community
|
||||||
attributes:
|
attributes:
|
||||||
label: Community Communication / 社区沟通
|
label: Before submitting
|
||||||
description: Confirm you will communicate respectfully and constructively / 确认将以礼貌、建设性的方式沟通
|
|
||||||
options:
|
options:
|
||||||
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
- label: I will be polite and respectful. / 我会保持礼貌与尊重。
|
||||||
required: true
|
required: true
|
||||||
- label: I will share constructive, actionable suggestions. / 我会提供建设性、可行动的建议。
|
|
||||||
required: true
|
|
||||||
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
|
- label: I have read the CODE_OF_CONDUCT.md and CONTRIBUTING.md. / 我已阅读行为准则。
|
||||||
required: true
|
required: true
|
||||||
|
- label: I have searched existing issues and this is not a duplicate.
|
||||||
|
required: true
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
|
|
||||||
Brief description of changes.
|
Brief description of changes.
|
||||||
|
|
||||||
|
Closes #(issue)
|
||||||
|
|
||||||
## Type
|
## Type
|
||||||
|
|
||||||
|
- [ ] Breaking change
|
||||||
- [ ] Bug fix
|
- [ ] Bug fix
|
||||||
- [ ] Feature / Improvement
|
- [ ] Feature / Improvement
|
||||||
- [ ] Refactor
|
- [ ] Refactor / Chores
|
||||||
- [ ] Documentation
|
- [ ] Documentation / Website / Demo / Testing
|
||||||
- [ ] Website
|
|
||||||
- [ ] Demo / Testing
|
|
||||||
- [ ] Breaking change
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
@@ -18,8 +18,6 @@ Brief description of changes.
|
|||||||
- [ ] No console errors
|
- [ ] No console errors
|
||||||
- [ ] Types/doc added
|
- [ ] Types/doc added
|
||||||
|
|
||||||
Closes #(issue)
|
|
||||||
|
|
||||||
## Requirements / 要求
|
## Requirements / 要求
|
||||||
|
|
||||||
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
- [ ] I have read and follow the [Code of Conduct](../docs/CODE_OF_CONDUCT.md) and [Contributing Guide](../CONTRIBUTING.md) . / 我已阅读并遵守行为准则。
|
||||||
|
|||||||
+11
-4
@@ -4,28 +4,35 @@ updates:
|
|||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'weekly'
|
interval: 'weekly'
|
||||||
|
labels:
|
||||||
|
- 'dependencies'
|
||||||
groups:
|
groups:
|
||||||
# 生产依赖 - 小版本更新
|
|
||||||
production-dependencies:
|
production-dependencies:
|
||||||
dependency-type: 'production'
|
dependency-type: 'production'
|
||||||
update-types:
|
update-types:
|
||||||
- 'minor'
|
- 'minor'
|
||||||
- 'patch'
|
- 'patch'
|
||||||
|
|
||||||
# 开发依赖 - 小版本更新
|
|
||||||
development-dependencies:
|
development-dependencies:
|
||||||
dependency-type: 'development'
|
dependency-type: 'development'
|
||||||
update-types:
|
update-types:
|
||||||
- 'minor'
|
- 'minor'
|
||||||
- 'patch'
|
- 'patch'
|
||||||
|
|
||||||
# Major 更新单独处理(不分组,需要人工审查)
|
development-major:
|
||||||
# 安全更新也不分组,Dependabot 会自动优先创建独立 PR
|
dependency-type: 'development'
|
||||||
|
update-types:
|
||||||
|
- 'major'
|
||||||
|
|
||||||
|
# Production major updates are intentionally ungrouped for individual review.
|
||||||
|
# Security updates are also ungrouped — Dependabot prioritizes them automatically.
|
||||||
|
|
||||||
- package-ecosystem: 'github-actions'
|
- package-ecosystem: 'github-actions'
|
||||||
directory: '/'
|
directory: '/'
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'weekly'
|
interval: 'weekly'
|
||||||
|
labels:
|
||||||
|
- 'dependencies'
|
||||||
groups:
|
groups:
|
||||||
github-actions:
|
github-actions:
|
||||||
patterns:
|
patterns:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
name: CI
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -17,6 +17,8 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js ${{ matrix.node-version }}
|
- name: Setup Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
@@ -24,18 +26,8 @@ jobs:
|
|||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
# test on default version of npm
|
|
||||||
# - 9.6~10.8 on node@20
|
|
||||||
# - 11.3~11.6 on node@24
|
|
||||||
|
|
||||||
- name: Node and NPM version
|
|
||||||
run: node --version && npm --version
|
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Lint
|
- name: CI checks
|
||||||
run: npx eslint . && npx prettier --check **/*.ts
|
run: node scripts/ci.js
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: npm run build
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
uses: actions/configure-pages@v6
|
uses: actions/configure-pages@v6
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v4
|
uses: actions/upload-pages-artifact@v5
|
||||||
with:
|
with:
|
||||||
path: './packages/website/dist'
|
path: './packages/website/dist'
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -25,6 +25,9 @@ dist-ssr
|
|||||||
*.sw?
|
*.sw?
|
||||||
.qoder
|
.qoder
|
||||||
|
|
||||||
|
# publish backup
|
||||||
|
package.json.bak
|
||||||
|
|
||||||
# env files
|
# env files
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
@@ -34,8 +37,6 @@ dist-ssr
|
|||||||
.wxt
|
.wxt
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
.agent
|
|
||||||
.claude
|
.claude
|
||||||
.cursor
|
.cursor
|
||||||
.gemini
|
.gemini
|
||||||
CLAUDE.md
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Prompt templates (formatted manually for LLM readability)
|
||||||
|
*prompt.md
|
||||||
|
|
||||||
|
# Generated
|
||||||
|
packages/extension/.wxt
|
||||||
|
|
||||||
|
# Vendored
|
||||||
|
**/components/ui
|
||||||
Vendored
+6
-2
@@ -22,7 +22,7 @@
|
|||||||
"packages/*/node_modules": true
|
"packages/*/node_modules": true
|
||||||
},
|
},
|
||||||
"markdownlint.config": {
|
"markdownlint.config": {
|
||||||
// "comment": "Relaxed rules",
|
// Relaxed rules
|
||||||
"default": true,
|
"default": true,
|
||||||
"whitespace": false,
|
"whitespace": false,
|
||||||
"line_length": false,
|
"line_length": false,
|
||||||
@@ -35,5 +35,9 @@
|
|||||||
"blanks-around-lists": false,
|
"blanks-around-lists": false,
|
||||||
"ol-prefix": false,
|
"ol-prefix": false,
|
||||||
"no-duplicate-heading": false
|
"no-duplicate-heading": false
|
||||||
}
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"js/ts.tsdk.path": "node_modules/typescript/lib",
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
This is a **monorepo** with npm workspaces:
|
This is a **monorepo** with npm workspaces:
|
||||||
|
|
||||||
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
||||||
- **Extension** (`packages/extension/`) - Browser extension (WXT + React) 🚧 WIP
|
- **Extension** (`packages/extension/`) - Browser extension (WXT + React)
|
||||||
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
|
- **Website** (`packages/website/`) - React docs and landing page. **When working on website, follow `packages/website/AGENTS.md`**
|
||||||
|
|
||||||
Internal packages:
|
Internal packages:
|
||||||
@@ -18,18 +18,19 @@ Internal packages:
|
|||||||
## Development Commands
|
## Development Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start # Start website dev server
|
npm start # Start website dev server
|
||||||
npm run build # Build all packages
|
npm run build # Build all packages
|
||||||
npm run build:libs # Build all libraries
|
npm run build:libs # Build all libraries
|
||||||
npm run lint # ESLint with TypeScript strict rules
|
npm run build:ext # Build and zip the extension package
|
||||||
npm run zip -w @page-agent/ext # Zip the extension package
|
npm run typecheck # Typecheck all packages
|
||||||
|
npm run lint # ESLint
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Monorepo Structure
|
### Monorepo Structure
|
||||||
|
|
||||||
Simple monorepo solution: TypeScript references + Vite aliases. Update tsconfig and vite config when adding/removing packages.
|
Source-first monorepo: library `package.json` exports point to `src/*.ts` during development. At publish time, `scripts/pre-publish.js` promotes `publishConfig` fields to top-level (swapping to `dist/`), and `scripts/post-publish.js` restores the originals.
|
||||||
|
|
||||||
```
|
```
|
||||||
packages/
|
packages/
|
||||||
@@ -37,7 +38,7 @@ packages/
|
|||||||
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
├── page-agent/ # npm: "page-agent" entry class (with UI + controller + demo builds)
|
||||||
├── website/ # @page-agent/website (private)
|
├── website/ # @page-agent/website (private)
|
||||||
├── llms/ # @page-agent/llms
|
├── llms/ # @page-agent/llms
|
||||||
├── extension/ # Browser extension (WXT + React)
|
├── extension/ # Browser extension
|
||||||
├── page-controller/ # @page-agent/page-controller
|
├── page-controller/ # @page-agent/page-controller
|
||||||
└── ui/ # @page-agent/ui
|
└── ui/ # @page-agent/ui
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -50,8 +50,10 @@ Fastest way to try PageAgent with our free Demo LLM:
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.8.2/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
Add `?autoInit=false` to load the script without creating the demo agent automatically. You can then instantiate it with `new window.PageAgent(...)`.
|
||||||
|
|
||||||
### NPM Installation
|
### NPM Installation
|
||||||
|
|
||||||
@@ -74,6 +76,16 @@ await agent.execute('Click the login button')
|
|||||||
|
|
||||||
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
|
For more programmatic usage, see [📖 Documentations](https://alibaba.github.io/page-agent/docs/introduction/overview).
|
||||||
|
|
||||||
|
## 🌟 Awesome Page Agent
|
||||||
|
|
||||||
|
Built something cool with PageAgent? Add it here! Open a PR to share your project.
|
||||||
|
|
||||||
|
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
|
||||||
|
|
||||||
|
| Project | Description |
|
||||||
|
| -------- | ----------------------------------------------------------- |
|
||||||
|
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
|
We welcome contributions from the community! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines and [docs/developer-guide.md](docs/developer-guide.md) for local development workflows.
|
||||||
@@ -104,16 +116,6 @@ excellent work on web automation and DOM interaction patterns that helped make
|
|||||||
this project possible.
|
this project possible.
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌟 Awesome Page Agent
|
|
||||||
|
|
||||||
Built something cool with PageAgent? Add it here! Open a PR to share your project.
|
|
||||||
|
|
||||||
> These are community projects — not maintained or endorsed by us. Use at your own discretion.
|
|
||||||
|
|
||||||
| Project | Description |
|
|
||||||
| -------- | ----------------------------------------------------------- |
|
|
||||||
| _Yours?_ | [Open a PR](https://github.com/alibaba/page-agent/pulls) 🙌 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**⭐ Star this repo if you find PageAgent helpful!**
|
**⭐ Star this repo if you find PageAgent helpful!**
|
||||||
|
|||||||
@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.1] - 2026-04-04
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Optional `keepSemanticTags`** - Added an experimental `keepSemanticTags` config to preserve semantic structure in PageController output
|
||||||
|
- **Per-task extension system instructions** - Extension `ExecuteConfig` now supports `systemInstruction`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- **Smarter scroll handling** - Scroll container detection and scroll direction handling are more reliable
|
||||||
|
- **Better accessibility-aware element detection** - Interactive candidates with supported ARIA attributes and `role="listitem"` are recognized more accurately
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed iframe-origin filtering for extension `postMessage` listeners
|
||||||
|
- Avoided a `currentScript` null pointer during deferred initialization
|
||||||
|
|
||||||
|
## [1.7.0] - 2026-03-31
|
||||||
|
|
||||||
|
- **More reliable click actions** - Click handling now reuses pointer coordinates, verifies targets with `elementFromPoint`, and behaves better on layered layouts
|
||||||
|
- **Better mask event handling** - `SimulatorMask` now supports passthrough events when automation should not fully swallow input
|
||||||
|
- Fixed a `SimulatorMask` memory leak
|
||||||
|
|
||||||
|
## [1.6.3] - 2026-03-30
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **Experimental all-tabs control** - Extension can include and control all browser tabs via `experimentalIncludeAllTabs`
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
|
||||||
|
- **Calmer empty state motion** - Disabled the EmptyState auto-start animation in the extension UI
|
||||||
|
- **Cleaner extension docs** - Simplified setup and tab-control documentation across the README and developer guide
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fixed new-tab detection from content scripts
|
||||||
|
- Fixed tab deduplication and multi-window handling in the extension
|
||||||
|
|
||||||
|
## [1.6.2] - 2026-03-25
|
||||||
|
|
||||||
|
- **Longer task input** - The UI task input now accepts up to 1000 characters
|
||||||
|
- **Contributor docs refresh** - Added a maintainer note and refreshed contributor-facing documentation
|
||||||
|
- Fixed lint issues in the release pipeline
|
||||||
|
|
||||||
|
## [1.6.1] - 2026-03-22
|
||||||
|
|
||||||
|
- **Internal PageController action exports** - PageController actions are now exposed as internal methods for easier reuse across packages
|
||||||
|
- **Expanded docs** - Added MCP docs and clarified project limitations and homepage details
|
||||||
|
|
||||||
## [1.6.0] - 2026-03-21
|
## [1.6.0] - 2026-03-21
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
+23
-23
@@ -16,22 +16,22 @@ appearance, race, religion, or sexual identity and orientation.
|
|||||||
Examples of behavior that contributes to creating a positive environment
|
Examples of behavior that contributes to creating a positive environment
|
||||||
include:
|
include:
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
- Using welcoming and inclusive language
|
||||||
* Being respectful of differing viewpoints and experiences
|
- Being respectful of differing viewpoints and experiences
|
||||||
* Gracefully accepting constructive criticism
|
- Gracefully accepting constructive criticism
|
||||||
* Focusing on what is best for the community
|
- Focusing on what is best for the community
|
||||||
* Showing empathy towards other community members
|
- Showing empathy towards other community members
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
advances
|
advances
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
- Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or electronic
|
- Publishing others' private information, such as a physical or electronic
|
||||||
address, without explicit permission
|
address, without explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
## Our Responsibilities
|
## Our Responsibilities
|
||||||
|
|
||||||
@@ -85,19 +85,19 @@ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.ht
|
|||||||
|
|
||||||
有助于创造正面环境的行为包括但不限于:
|
有助于创造正面环境的行为包括但不限于:
|
||||||
|
|
||||||
* 使用友好和包容性语言
|
- 使用友好和包容性语言
|
||||||
* 尊重不同的观点和经历
|
- 尊重不同的观点和经历
|
||||||
* 耐心地接受建设性批评
|
- 耐心地接受建设性批评
|
||||||
* 关注对社区最有利的事情
|
- 关注对社区最有利的事情
|
||||||
* 友善对待其他社区成员
|
- 友善对待其他社区成员
|
||||||
|
|
||||||
身为参与者不能接受的行为包括但不限于:
|
身为参与者不能接受的行为包括但不限于:
|
||||||
|
|
||||||
* 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
|
- 使用与性有关的言语或是图像,以及不受欢迎的性骚扰
|
||||||
* 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
|
- 捣乱/煽动/造谣的行为或进行侮辱/贬损的评论,人身攻击及政治攻击
|
||||||
* 公开或私下的骚扰
|
- 公开或私下的骚扰
|
||||||
* 未经许可地发布他人的个人资料,例如住址或是电子地址
|
- 未经许可地发布他人的个人资料,例如住址或是电子地址
|
||||||
* 其他可以被合理地认定为不恰当或者违反职业操守的行为
|
- 其他可以被合理地认定为不恰当或者违反职业操守的行为
|
||||||
|
|
||||||
## 我们的责任
|
## 我们的责任
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -49,8 +49,10 @@
|
|||||||
|
|
||||||
| Mirrors | URL |
|
| Mirrors | URL |
|
||||||
| ------- | ---------------------------------------------------------------------------------- |
|
| ------- | ---------------------------------------------------------------------------------- |
|
||||||
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.7.0/dist/iife/page-agent.demo.js |
|
| Global | https://cdn.jsdelivr.net/npm/page-agent@1.8.2/dist/iife/page-agent.demo.js |
|
||||||
| China | https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js |
|
| China | https://registry.npmmirror.com/page-agent/1.8.2/files/dist/iife/page-agent.demo.js |
|
||||||
|
|
||||||
|
在 URL 后添加 `?autoInit=false` 可只加载脚本,不自动创建 Demo Agent;之后可通过 `new window.PageAgent(...)` 手动初始化。
|
||||||
|
|
||||||
### NPM 安装
|
### NPM 安装
|
||||||
|
|
||||||
@@ -106,4 +108,3 @@ this project possible.
|
|||||||
---
|
---
|
||||||
|
|
||||||
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
**⭐ 如果觉得 PageAgent 有用或有趣,请给项目点个星!**
|
||||||
|
|
||||||
|
|||||||
+21
-33
@@ -10,52 +10,41 @@ For contribution rules and expectations, see [../CONTRIBUTING.md](../CONTRIBUTIN
|
|||||||
|
|
||||||
1. **Prerequisites**
|
1. **Prerequisites**
|
||||||
- `macOS` / `Linux` / `WSL`
|
- `macOS` / `Linux` / `WSL`
|
||||||
- `node.js >= 20` with `npm >= 10`
|
- `node.js ^22.13 || >=24` with `npm >= 11`
|
||||||
- An editor that supports `ts/eslint/prettier`
|
- An editor that supports `ts/eslint/prettier`
|
||||||
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
|
- Make sure `eslint`, `prettier` and `commitlint` work well. Un-linted code won't pass the CI.
|
||||||
|
|
||||||
2. **Setup**
|
2. **Setup**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i
|
npm i # Or `npm ci` if you don't want to change the lockfile
|
||||||
npm start # Start demo and documentation site
|
npm start # Start website dev server
|
||||||
npm run build # Build libs and website
|
npm run build # Build everything
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📦 Project Structure
|
## 📦 Project Structure
|
||||||
|
|
||||||
This is a **monorepo** with npm workspaces containing **4 main packages**:
|
This is a **monorepo** with npm workspaces.
|
||||||
|
|
||||||
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel, published as `page-agent` on npm
|
Published packages:
|
||||||
|
|
||||||
|
- **Page Agent** (`packages/page-agent/`) - Main entry with built-in UI Panel (npm: `page-agent`)
|
||||||
|
- **MCP** (`packages/mcp/`) - MCP server for browser control via Page Agent extension (npm: `@page-agent/mcp`)
|
||||||
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
- **Core** (`packages/core/`) - Core agent logic without UI (npm: `@page-agent/core`)
|
||||||
- **Extension** (`packages/extension/`) - Chrome extension for multi-page tasks and browser-level automation
|
- **LLMs** (`packages/llms/`) - LLM client with reflection-before-action mental model
|
||||||
- **Website** (`packages/website/`) - React documentation and landing page. Also as demo and test page for the core lib. private package `@page-agent/website`
|
- **Page Controller** (`packages/page-controller/`) - DOM operations and visual feedback, independent of LLM
|
||||||
|
- **UI** (`packages/ui/`) - Panel and i18n, decoupled from PageAgent
|
||||||
|
|
||||||
> We use a simplified monorepo solution with `native npm-workspace + ts reference + vite alias`. No fancy tooling. Hoisting is required.
|
Applications:
|
||||||
>
|
|
||||||
> - When developing. Use alias so that we don't have to pre-build.
|
- **Extension** (`packages/extension/`) - Browser extension (WXT + React)
|
||||||
> - When bundling. Use external and disable ts `paths` alias.
|
- **Website** (`packages/website/`) - React docs, landing page, and dev playground (private)
|
||||||
> - When bundling `IIFE` and `Website`. Bundle everything together.
|
|
||||||
|
> Source-first monorepo with `npm workspaces + ts references + vite alias`. Library `package.json` exports point to `src/*.ts` during development, and point to `dist/*.js` when published. `workspaces` in root `package.json` must be in topological order.
|
||||||
|
|
||||||
## 🤖 AGENTS.md Alias
|
## 🤖 AGENTS.md Alias
|
||||||
|
|
||||||
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it:
|
If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an alias for it.
|
||||||
|
|
||||||
- claude-code (`CLAUDE.md`)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
@AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
- antigravity (`.agent/rules/alias.md`)
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
trigger: always_on
|
|
||||||
---
|
|
||||||
|
|
||||||
@../../AGENTS.md
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔧 Development Workflows
|
## 🔧 Development Workflows
|
||||||
|
|
||||||
@@ -85,9 +74,8 @@ If your AI assistant does not support [AGENTS.md](https://agents.md/). Add an al
|
|||||||
### Extension Development
|
### Extension Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# make sure you ran `npm run build:libs` first and every time you changed the core libs
|
npm run dev:ext
|
||||||
npm run dev -w @page-agent/ext
|
npm run build:ext
|
||||||
npm run zip -w @page-agent/ext
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Update `packages/extension/docs/extension_api.md` for API integration details
|
- Update `packages/extension/docs/extension_api.md` for API integration details
|
||||||
|
|||||||
+6
-29
@@ -1,8 +1,5 @@
|
|||||||
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
import js from '@eslint/js'
|
import js from '@eslint/js'
|
||||||
import reactDom from 'eslint-plugin-react-dom'
|
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
|
||||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
||||||
import reactX from 'eslint-plugin-react-x'
|
|
||||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
import globals from 'globals'
|
import globals from 'globals'
|
||||||
import tseslint from 'typescript-eslint'
|
import tseslint from 'typescript-eslint'
|
||||||
@@ -15,44 +12,24 @@ export default defineConfig([
|
|||||||
'**/.wxt',
|
'**/.wxt',
|
||||||
'**/.output',
|
'**/.output',
|
||||||
]),
|
]),
|
||||||
{
|
|
||||||
plugins: {
|
|
||||||
'react-hooks': reactHooks,
|
|
||||||
},
|
|
||||||
rules: reactHooks.configs.recommended.rules,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended,
|
||||||
// reactHooks.configs['recommended-latest'],
|
|
||||||
reactRefresh.configs.vite,
|
|
||||||
|
|
||||||
// Remove tseslint.configs.recommended and replace with this
|
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
// Alternatively, use this for stricter rules
|
|
||||||
...tseslint.configs.strictTypeChecked,
|
...tseslint.configs.strictTypeChecked,
|
||||||
// Optionally, add this for stylistic rules
|
|
||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
eslintReact.configs['recommended-typescript'],
|
||||||
// Enable lint rules for React
|
|
||||||
reactX.configs['recommended-typescript'],
|
|
||||||
// Enable lint rules for React DOM
|
|
||||||
reactDom.configs.recommended,
|
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
// project: ['./tsconfig.json'],
|
|
||||||
// project: ['./packages/*/tsconfig.json'],
|
|
||||||
// tsconfigRootDir: import.meta.dirname,
|
|
||||||
projectService: true,
|
projectService: true,
|
||||||
},
|
},
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// Add any additional rules here
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
@@ -72,14 +49,14 @@ export default defineConfig([
|
|||||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||||
'@typescript-eslint/no-unsafe-return': 'off',
|
'@typescript-eslint/no-unsafe-return': 'off',
|
||||||
'@typescript-eslint/restrict-plus-operands': 'off',
|
'@typescript-eslint/restrict-plus-operands': 'off',
|
||||||
'react-dom/no-missing-button-type': 'off',
|
|
||||||
'react-x/no-nested-component-definitions': 'off',
|
|
||||||
'@typescript-eslint/prefer-optional-chain': 'off',
|
'@typescript-eslint/prefer-optional-chain': 'off',
|
||||||
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
'@typescript-eslint/use-unknown-in-catch-callback-variable': 'off',
|
||||||
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
|
'@typescript-eslint/no-unnecessary-type-parameters': 'off',
|
||||||
|
|
||||||
// 'require-await': 'off',
|
|
||||||
'@typescript-eslint/require-await': 'off',
|
'@typescript-eslint/require-await': 'off',
|
||||||
|
'@eslint-react/dom-no-missing-button-type': 'off',
|
||||||
|
'@eslint-react/no-nested-component-definitions': 'off',
|
||||||
|
'@eslint-react/no-array-index-key': 'off',
|
||||||
|
'@eslint-react/dom-no-dangerously-set-innerhtml': 'off',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
Generated
+1864
-2627
File diff suppressed because it is too large
Load Diff
+29
-30
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "root",
|
"name": "root",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/page-controller",
|
"packages/page-controller",
|
||||||
@@ -22,52 +22,51 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^22.13.0 || >=24",
|
||||||
|
"npm": "^11.6.3"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run dev --workspace=@page-agent/website",
|
"start": "npm run dev --workspace=@page-agent/website",
|
||||||
"dev:ext": "npm run dev -w @page-agent/ext",
|
"dev:ext": "npm run dev -w @page-agent/ext",
|
||||||
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
"dev:demo": "npm run dev:demo --workspace=page-agent",
|
||||||
"build": "npm run build:libs && npm run build:website",
|
"build": "node scripts/build.js",
|
||||||
"build:libs": "npm run build --workspaces --if-present",
|
"build:libs": "node scripts/build-libs.js",
|
||||||
"build:website": "npm run build:website --workspace=@page-agent/website",
|
"build:website": "npm run build:website --workspace=@page-agent/website",
|
||||||
"build:ext": "npm run build:libs && npm run zip -w @page-agent/ext",
|
"build:ext": "npm run zip -w @page-agent/ext",
|
||||||
"version": "node scripts/sync-version.js",
|
"version": "node scripts/sync-version.js",
|
||||||
|
"postpublish": "npm run postpublish --workspaces --if-present",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.typecheck.json && tsc --noEmit -p packages/extension/tsconfig.json",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"cleanup": "rm -rf packages/*/dist",
|
"ci": "node scripts/ci.js",
|
||||||
"prepare": "husky"
|
"cleanup": "rm -rf packages/*/dist && rm -rf packages/*/.output",
|
||||||
|
"prepare": "husky || true"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^20.5.0",
|
"@commitlint/cli": "^20.5.3",
|
||||||
"@commitlint/config-conventional": "^20.5.0",
|
"@commitlint/config-conventional": "^20.5.3",
|
||||||
"@eslint/js": "^9.39.2",
|
"@eslint-react/eslint-plugin": "^4.2.3",
|
||||||
"@microsoft/api-extractor": "^7.57.7",
|
"@eslint/js": "^10.0.1",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@microsoft/api-extractor": "^7.58.7",
|
||||||
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.6.0",
|
||||||
"@vitejs/plugin-react-swc": "^4.3.0",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.4.2",
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^10.2.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"globals": "^17.5.0",
|
||||||
"eslint-plugin-react-dom": "^2.13.0",
|
|
||||||
"eslint-plugin-react-hooks": "^7.0.1",
|
|
||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
|
||||||
"eslint-plugin-react-x": "^2.13.0",
|
|
||||||
"globals": "^17.4.0",
|
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"lint-staged": "^16.4.0",
|
"lint-staged": "^16.4.0",
|
||||||
"prettier": "^3.8.0",
|
"prettier": "^3.8.2",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.59.2",
|
||||||
"unplugin-dts": "^1.0.0-beta.6",
|
"unplugin-dts": "^1.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^8.0.12",
|
||||||
"vite-plugin-css-injected-by-js": "^4.0.1",
|
"vite-plugin-css-injected-by-js": "^5.0.1"
|
||||||
"vite-bundle-analyzer": "^1.3.6"
|
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^6.0.2"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,cjs,cts,mjs,mts}": [
|
"*.{js,ts,cjs,cts,mjs,mts}": [
|
||||||
|
|||||||
+22
-12
@@ -1,16 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/core",
|
"name": "@page-agent/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent-core.js",
|
"main": "./src/PageAgentCore.ts",
|
||||||
"module": "./dist/esm/page-agent-core.js",
|
"types": "./src/PageAgentCore.ts",
|
||||||
"types": "./dist/esm/PageAgentCore.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/esm/PageAgentCore.d.ts",
|
"types": "./src/PageAgentCore.ts",
|
||||||
"import": "./dist/esm/page-agent-core.js",
|
"default": "./src/PageAgentCore.ts"
|
||||||
"default": "./dist/esm/page-agent-core.js"
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/esm/page-agent-core.js",
|
||||||
|
"module": "./dist/esm/page-agent-core.js",
|
||||||
|
"types": "./dist/esm/PageAgentCore.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/PageAgentCore.d.ts",
|
||||||
|
"import": "./dist/esm/page-agent-core.js",
|
||||||
|
"default": "./dist/esm/page-agent-core.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -39,18 +49,18 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
"dev:iife": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2",
|
"chalk": "^5.6.2",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.7.0"
|
"@page-agent/page-controller": "1.8.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
system_prompt.md
|
|
||||||
@@ -131,7 +131,8 @@ tools.set(
|
|||||||
tools.set(
|
tools.set(
|
||||||
'scroll',
|
'scroll',
|
||||||
tool({
|
tool({
|
||||||
description: 'Scroll the page vertically. Use index for scroll elements (dropdowns/custom UI).',
|
description:
|
||||||
|
'Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
down: z.boolean().default(true),
|
down: z.boolean().default(true),
|
||||||
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
num_pages: z.number().min(0).max(10).optional().default(0.1),
|
||||||
@@ -155,7 +156,7 @@ tools.set(
|
|||||||
'scroll_horizontally',
|
'scroll_horizontally',
|
||||||
tool({
|
tool({
|
||||||
description:
|
description:
|
||||||
'Scroll the page horizontally, or within a specific element by index. Useful for wide tables.',
|
'Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.',
|
||||||
inputSchema: z.object({
|
inputSchema: z.object({
|
||||||
right: z.boolean().default(true),
|
right: z.boolean().default(true),
|
||||||
pixels: z.number().int().min(0),
|
pixels: z.number().int().min(0),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InvokeError, InvokeErrorType } from '@page-agent/llms'
|
import { InvokeError, InvokeErrorTypes } from '@page-agent/llms'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ const log = console.log.bind(console, chalk.yellow('[autoFixer]'))
|
|||||||
* - etc.
|
* - etc.
|
||||||
*/
|
*/
|
||||||
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
|
export function normalizeResponse(response: any, tools?: Map<string, PageAgentTool>): any {
|
||||||
let resolvedArguments = null as any
|
let resolvedArguments: any
|
||||||
|
|
||||||
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
const choice = (response as { choices?: Choice[] }).choices?.[0]
|
||||||
if (!choice) throw new Error('No choices in response')
|
if (!choice) throw new Error('No choices in response')
|
||||||
@@ -93,7 +93,7 @@ export function normalizeResponse(response: any, tools?: Map<string, PageAgentTo
|
|||||||
// fix incomplete formats
|
// fix incomplete formats
|
||||||
if (!resolvedArguments.action) {
|
if (!resolvedArguments.action) {
|
||||||
log(`#5: fixing tool_call`)
|
log(`#5: fixing tool_call`)
|
||||||
resolvedArguments.action = { name: 'wait', input: { seconds: 1 } }
|
resolvedArguments.action = { wait: { seconds: 1 } }
|
||||||
}
|
}
|
||||||
|
|
||||||
// pack back to standard format
|
// pack back to standard format
|
||||||
@@ -137,7 +137,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
|||||||
if (!tool) {
|
if (!tool) {
|
||||||
const available = Array.from(tools.keys()).join(', ')
|
const available = Array.from(tools.keys()).join(', ')
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
`Unknown action "${toolName}". Available: ${available}`
|
`Unknown action "${toolName}". Available: ${available}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ function validateAction(action: any, tools: Map<string, PageAgentTool>): any {
|
|||||||
const result = schema.safeParse(value)
|
const result = schema.safeParse(value)
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
`Invalid input for action "${toolName}": ${z.prettifyError(result.error)}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
// @workaround DTS bug
|
|
||||||
// dts do not work with monorepo path mapping
|
|
||||||
// disable path mapping for it
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist",
|
|
||||||
"paths": {
|
|
||||||
//
|
|
||||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
|
||||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"],
|
"exclude": ["dist", "node_modules"]
|
||||||
"references": [
|
|
||||||
//
|
|
||||||
{ "path": "../llms" },
|
|
||||||
{ "path": "../page-controller" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
dts({
|
||||||
|
bundleTypes: true,
|
||||||
|
compilerOptions: {
|
||||||
|
composite: true,
|
||||||
|
noEmit: false,
|
||||||
|
emitDeclarationOnly: true,
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
],
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
|
entry: resolve(__dirname, 'src/PageAgentCore.ts'),
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
.wxt
|
|
||||||
src/components/ui
|
|
||||||
@@ -42,33 +42,28 @@ localStorage.setItem('PageAgentExtUserAuthToken', 'your-token')
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
// Wait for extension injection (up to 1 second)
|
// Wait for extension injection (up to 1 second)
|
||||||
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
async function waitForExtension(timeout = 1000): Promise<boolean> {
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
while (Date.now() - start < timeout) {
|
while (Date.now() - start < timeout) {
|
||||||
if (window.PAGE_AGENT_EXT) return true
|
if (window.PAGE_AGENT_EXT) return true
|
||||||
await new Promise((r) => setTimeout(r, 100))
|
await new Promise((r) => setTimeout(r, 100))
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
if (await waitForExtension()) {
|
if (await waitForExtension()) {
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
|
const result = await window.PAGE_AGENT_EXT!.execute('Click the login button', {
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
onStatusChange: (status) => console.log('Status:', status),
|
onStatusChange: (status) => console.log('Status:', status),
|
||||||
onActivity: (activity) => console.log('Activity:', activity),
|
onActivity: (activity) => console.log('Activity:', activity),
|
||||||
})
|
})
|
||||||
console.log('Result:', result)
|
console.log('Result:', result)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -90,10 +85,10 @@ Execute one agent task.
|
|||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
||||||
| Name | Type | Required | Description |
|
| Name | Type | Required | Description |
|
||||||
| ---- | ---- | -------- | ----------- |
|
| -------- | --------------- | -------- | ------------------------------------ |
|
||||||
| `task` | `string` | Yes | Task description |
|
| `task` | `string` | Yes | Task description |
|
||||||
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
| `config` | `ExecuteConfig` | Yes | LLM settings, options, and callbacks |
|
||||||
|
|
||||||
Returns: `Promise<ExecutionResult>`
|
Returns: `Promise<ExecutionResult>`
|
||||||
|
|
||||||
@@ -106,29 +101,28 @@ Stop the current task.
|
|||||||
Install `@page-agent/core` for complete types:
|
Install `@page-agent/core` for complete types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
export interface ExecuteConfig {
|
export interface ExecuteConfig {
|
||||||
baseURL: string
|
baseURL: string
|
||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
// Include the initial tab where page JS starts. Default: true.
|
// Global system-level instructions for the agent.
|
||||||
includeInitialTab?: boolean
|
// Equivalent to AgentConfig.instructions.system.
|
||||||
|
systemInstruction?: string
|
||||||
|
|
||||||
// Control all unpinned tabs in the window instead of only the tab group.
|
// Include the initial tab where page JS starts. Default: true.
|
||||||
// When enabled, agent sees and can switch to every non-pinned tab.
|
includeInitialTab?: boolean
|
||||||
// Default: false. Experimental.
|
|
||||||
experimentalIncludeAllTabs?: boolean
|
|
||||||
|
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
// Control all unpinned tabs in the window instead of only the tab group.
|
||||||
onActivity?: (activity: AgentActivity) => void
|
// When enabled, agent sees and can switch to every non-pinned tab.
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
// Default: false. Experimental.
|
||||||
|
experimentalIncludeAllTabs?: boolean
|
||||||
|
|
||||||
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
onActivity?: (activity: AgentActivity) => void
|
||||||
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
export type Execute = (task: string, config: ExecuteConfig) => Promise<ExecutionResult>
|
||||||
@@ -144,31 +138,31 @@ type AgentStatus = 'idle' | 'running' | 'completed' | 'error'
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type AgentActivity =
|
type AgentActivity =
|
||||||
| { type: 'thinking' }
|
| { type: 'thinking' }
|
||||||
| { type: 'executing'; tool: string; input: unknown }
|
| { type: 'executing'; tool: string; input: unknown }
|
||||||
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
| { type: 'executed'; tool: string; input: unknown; output: string; duration: number }
|
||||||
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
| { type: 'retrying'; attempt: number; maxAttempts: number }
|
||||||
| { type: 'error'; message: string }
|
| { type: 'error'; message: string }
|
||||||
```
|
```
|
||||||
|
|
||||||
`HistoricalEvent`
|
`HistoricalEvent`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
type HistoricalEvent =
|
type HistoricalEvent =
|
||||||
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
| { type: 'step'; stepIndex: number; reflection: AgentReflection; action: Action }
|
||||||
| { type: 'observation'; content: string }
|
| { type: 'observation'; content: string }
|
||||||
| { type: 'user_takeover' }
|
| { type: 'user_takeover' }
|
||||||
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
| { type: 'retry'; message: string; attempt: number; maxAttempts: number }
|
||||||
| { type: 'error'; message: string; rawResponse?: unknown }
|
| { type: 'error'; message: string; rawResponse?: unknown }
|
||||||
```
|
```
|
||||||
|
|
||||||
`ExecutionResult`
|
`ExecutionResult`
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
interface ExecutionResult {
|
interface ExecutionResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
data: string
|
data: string
|
||||||
history: HistoricalEvent[]
|
history: HistoricalEvent[]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -178,15 +172,15 @@ interface ExecutionResult {
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const result = await window.PAGE_AGENT_EXT!.execute(
|
const result = await window.PAGE_AGENT_EXT!.execute(
|
||||||
'Fill in the email field with test@example.com and click Submit',
|
'Fill in the email field with test@example.com and click Submit',
|
||||||
{
|
{
|
||||||
baseURL: 'https://api.openai.com/v1',
|
baseURL: 'https://api.openai.com/v1',
|
||||||
apiKey: process.env.OPENAI_API_KEY!,
|
apiKey: process.env.OPENAI_API_KEY!,
|
||||||
model: 'gpt-5.2',
|
model: 'gpt-5.2',
|
||||||
includeInitialTab: false, // Optional: exclude current tab
|
includeInitialTab: false, // Optional: exclude current tab
|
||||||
onStatusChange: (status) => console.log(status),
|
onStatusChange: (status) => console.log(status),
|
||||||
onActivity: (activity) => console.log(activity),
|
onActivity: (activity) => console.log(activity),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -201,32 +195,30 @@ window.PAGE_AGENT_EXT!.stop()
|
|||||||
If you are not importing `@page-agent/core`, add:
|
If you are not importing `@page-agent/core`, add:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type {
|
import type { AgentActivity, AgentStatus, ExecutionResult, HistoricalEvent } from '@page-agent/core'
|
||||||
AgentActivity,
|
|
||||||
AgentStatus,
|
|
||||||
ExecutionResult,
|
|
||||||
HistoricalEvent,
|
|
||||||
} from '@page-agent/core'
|
|
||||||
|
|
||||||
interface ExecuteConfig {
|
interface ExecuteConfig {
|
||||||
baseURL: string
|
baseURL: string
|
||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
includeInitialTab?: boolean
|
|
||||||
experimentalIncludeAllTabs?: boolean
|
systemInstruction?: string
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
|
||||||
onActivity?: (activity: AgentActivity) => void
|
includeInitialTab?: boolean
|
||||||
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
experimentalIncludeAllTabs?: boolean
|
||||||
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
onActivity?: (activity: AgentActivity) => void
|
||||||
|
onHistoryUpdate?: (history: HistoricalEvent[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
PAGE_AGENT_EXT_VERSION?: string
|
PAGE_AGENT_EXT_VERSION?: string
|
||||||
PAGE_AGENT_EXT?: {
|
PAGE_AGENT_EXT?: {
|
||||||
version: string
|
version: string
|
||||||
execute: Execute
|
execute: Execute
|
||||||
stop: () => void
|
stop: () => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ext",
|
"name": "@page-agent/ext",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wxt",
|
"dev": "wxt",
|
||||||
@@ -16,31 +16,32 @@
|
|||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@types/chrome": "^0.1.38",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
"@types/chrome": "^0.1.42",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.1",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@wxt-dev/module-react": "^1.2.2",
|
"@wxt-dev/module-react": "^1.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"idb": "^8.0.3",
|
"idb": "^8.0.3",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.14.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.14.0",
|
"simple-icons": "^16.19.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wxt": "^0.20.20"
|
"wxt": "^0.20.25"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.8.2",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.8.2",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.8.2",
|
||||||
"ai-motion": "^0.4.8",
|
"ai-motion": "^0.4.8",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extName": {
|
"extName": {
|
||||||
"message": "Page Agent Ext"
|
"message": "Page Agent Ext"
|
||||||
},
|
},
|
||||||
"extDescription": {
|
"extDescription": {
|
||||||
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
"message": "AI-powered browser automation assistant. Control web pages with natural language."
|
||||||
},
|
},
|
||||||
"extActionTitle": {
|
"extActionTitle": {
|
||||||
"message": "Open Page Agent"
|
"message": "Open Page Agent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extName": {
|
"extName": {
|
||||||
"message": "Page Agent Ext"
|
"message": "Page Agent Ext"
|
||||||
},
|
},
|
||||||
"extDescription": {
|
"extDescription": {
|
||||||
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
"message": "AI 驱动的浏览器自动化助手,用自然语言控制网页。"
|
||||||
},
|
},
|
||||||
"extActionTitle": {
|
"extActionTitle": {
|
||||||
"message": "打开 Page Agent"
|
"message": "打开 Page Agent"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
system_prompt.md
|
|
||||||
@@ -19,7 +19,10 @@ export function initPageController() {
|
|||||||
|
|
||||||
function getPC(): PageController {
|
function getPC(): PageController {
|
||||||
if (!pageController) {
|
if (!pageController) {
|
||||||
pageController = new PageController({ enableMask: false, viewportExpansion: 400 })
|
pageController = new PageController({
|
||||||
|
enableMask: false,
|
||||||
|
viewportExpansion: 400,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return pageController
|
return pageController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class RemotePageController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getBrowserState(): Promise<BrowserState> {
|
async getBrowserState(): Promise<BrowserState> {
|
||||||
let browserState = {} as BrowserState
|
let browserState: BrowserState
|
||||||
debug('getBrowserState', this.currentTabId)
|
debug('getBrowserState', this.currentTabId)
|
||||||
|
|
||||||
const currentUrl = await this.getCurrentUrl()
|
const currentUrl = await this.getCurrentUrl()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export class TabsController {
|
|||||||
currentTabId: number | null = null
|
currentTabId: number | null = null
|
||||||
|
|
||||||
private disposed = false
|
private disposed = false
|
||||||
private port: chrome.runtime.Port | null = null
|
private port?: chrome.runtime.Port
|
||||||
private portRetries = 0
|
private portRetries = 0
|
||||||
|
|
||||||
private windowId: number | null = null
|
private windowId: number | null = null
|
||||||
@@ -44,7 +44,7 @@ export class TabsController {
|
|||||||
|
|
||||||
this.currentTabId = null
|
this.currentTabId = null
|
||||||
this.disposed = false
|
this.disposed = false
|
||||||
this.port = null
|
this.port = undefined
|
||||||
this.portRetries = 0
|
this.portRetries = 0
|
||||||
|
|
||||||
this.windowId = null
|
this.windowId = null
|
||||||
@@ -338,10 +338,10 @@ export class TabsController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.port.onDisconnect.addListener(() => {
|
this.port.onDisconnect.addListener(() => {
|
||||||
this.port = null
|
this.port = undefined
|
||||||
if (this.disposed) return
|
if (this.disposed) return
|
||||||
if (this.portRetries >= 7) {
|
if (this.portRetries >= 7) {
|
||||||
console.error(PREFIX, 'tab events port failed after 3 retries, giving up')
|
console.error(PREFIX, 'tab events port failed after 7 retries, giving up')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
debug('port disconnected, reconnecting...')
|
debug('port disconnected, reconnecting...')
|
||||||
@@ -354,7 +354,7 @@ export class TabsController {
|
|||||||
debug('dispose')
|
debug('dispose')
|
||||||
this.disposed = true
|
this.disposed = true
|
||||||
this.port?.disconnect()
|
this.port?.disconnect()
|
||||||
this.port = null
|
this.port = undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ export function useAgent(): UseAgentResult {
|
|||||||
|
|
||||||
const execute = useCallback(async (task: string) => {
|
const execute = useCallback(async (task: string) => {
|
||||||
const agent = agentRef.current
|
const agent = agentRef.current
|
||||||
console.log('🚀 [useAgent] start executing task:', task)
|
|
||||||
if (!agent) throw new Error('Agent not initialized')
|
if (!agent) throw new Error('Agent not initialized')
|
||||||
|
|
||||||
setCurrentTask(task)
|
setCurrentTask(task)
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
const [showToken, setShowToken] = useState(false)
|
const [showToken, setShowToken] = useState(false)
|
||||||
const [showApiKey, setShowApiKey] = useState(false)
|
const [showApiKey, setShowApiKey] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
const [prevConfig, setPrevConfig] = useState(config)
|
||||||
|
if (prevConfig !== config) {
|
||||||
|
setPrevConfig(config)
|
||||||
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
setBaseURL(config?.baseURL || DEMO_BASE_URL)
|
||||||
setModel(config?.model || DEMO_MODEL)
|
setModel(config?.model || DEMO_MODEL)
|
||||||
setApiKey(config?.apiKey)
|
setApiKey(config?.apiKey)
|
||||||
@@ -59,7 +61,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
|
setExperimentalLlmsTxt(config?.experimentalLlmsTxt ?? false)
|
||||||
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
|
setExperimentalIncludeAllTabs(config?.experimentalIncludeAllTabs ?? false)
|
||||||
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
|
setDisableNamedToolChoice(config?.disableNamedToolChoice ?? false)
|
||||||
}, [config])
|
}
|
||||||
|
|
||||||
// Poll for user auth token every second until found
|
// Poll for user auth token every second until found
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -121,6 +123,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-2 right-3 cursor-pointer"
|
className="absolute top-2 right-3 cursor-pointer"
|
||||||
|
aria-label="Back"
|
||||||
>
|
>
|
||||||
<CornerUpLeft className="size-3.5" />
|
<CornerUpLeft className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -128,12 +131,15 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
|
|
||||||
{/* User Auth Token Section */}
|
{/* User Auth Token Section */}
|
||||||
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
<div className="flex flex-col gap-1.5 p-3 bg-muted/50 rounded-md border">
|
||||||
<label className="text-xs font-medium text-muted-foreground">User Auth Token</label>
|
<label htmlFor="user-auth-token" className="text-xs font-medium text-muted-foreground">
|
||||||
|
User Auth Token
|
||||||
|
</label>
|
||||||
<p className="text-[10px] text-muted-foreground mb-1">
|
<p className="text-[10px] text-muted-foreground mb-1">
|
||||||
Give a website the ability to call this extension.
|
Give a website the ability to call this extension.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
|
id="user-auth-token"
|
||||||
readOnly
|
readOnly
|
||||||
value={
|
value={
|
||||||
userAuthToken
|
userAuthToken
|
||||||
@@ -150,6 +156,8 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={() => setShowToken(!showToken)}
|
onClick={() => setShowToken(!showToken)}
|
||||||
disabled={!userAuthToken}
|
disabled={!userAuthToken}
|
||||||
|
aria-label={showToken ? 'Hide token' : 'Show token'}
|
||||||
|
aria-pressed={showToken}
|
||||||
>
|
>
|
||||||
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
{showToken ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -159,9 +167,13 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={handleCopyToken}
|
onClick={handleCopyToken}
|
||||||
disabled={!userAuthToken}
|
disabled={!userAuthToken}
|
||||||
|
aria-label="Copy token"
|
||||||
>
|
>
|
||||||
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
|
{copied ? <span className="">✓</span> : <Copy className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
<span role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
||||||
|
{copied ? 'Token copied' : ''}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -169,6 +181,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
<a
|
<a
|
||||||
href="/hub.html"
|
href="/hub.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
className="flex items-center justify-between p-3 rounded-md border bg-muted/50 text-xs font-medium text-muted-foreground hover:text-foreground hover:border-foreground/20 transition-colors"
|
||||||
>
|
>
|
||||||
Manage Page Agent Hub
|
Manage Page Agent Hub
|
||||||
@@ -176,8 +189,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Base URL</label>
|
<label htmlFor="base-url" className="text-xs text-muted-foreground">
|
||||||
|
Base URL
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="base-url"
|
||||||
placeholder="https://api.openai.com/v1"
|
placeholder="https://api.openai.com/v1"
|
||||||
value={baseURL}
|
value={baseURL}
|
||||||
onChange={(e) => setBaseURL(e.target.value)}
|
onChange={(e) => setBaseURL(e.target.value)}
|
||||||
@@ -202,8 +218,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Model</label>
|
<label htmlFor="model" className="text-xs text-muted-foreground">
|
||||||
|
Model
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="model"
|
||||||
placeholder="gpt-5.1"
|
placeholder="gpt-5.1"
|
||||||
value={model}
|
value={model}
|
||||||
onChange={(e) => setModel(e.target.value)}
|
onChange={(e) => setModel(e.target.value)}
|
||||||
@@ -212,9 +231,12 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">API Key</label>
|
<label htmlFor="api-key" className="text-xs text-muted-foreground">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Input
|
<Input
|
||||||
|
id="api-key"
|
||||||
type={showApiKey ? 'text' : 'password'}
|
type={showApiKey ? 'text' : 'password'}
|
||||||
// placeholder="sk-..."
|
// placeholder="sk-..."
|
||||||
value={apiKey}
|
value={apiKey}
|
||||||
@@ -226,6 +248,7 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0 cursor-pointer"
|
className="h-8 w-8 shrink-0 cursor-pointer"
|
||||||
onClick={() => setShowApiKey(!showApiKey)}
|
onClick={() => setShowApiKey(!showApiKey)}
|
||||||
|
aria-label={showApiKey ? 'Hide API key' : 'Show API key'}
|
||||||
>
|
>
|
||||||
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
{showApiKey ? <EyeOff className="size-3" /> : <Eye className="size-3" />}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -258,8 +281,11 @@ export function ConfigPanel({ config, onSave, onClose }: ConfigPanelProps) {
|
|||||||
{advancedOpen && (
|
{advancedOpen && (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<label className="text-xs text-muted-foreground">Max Steps</label>
|
<label htmlFor="max-steps" className="text-xs text-muted-foreground">
|
||||||
|
Max Steps
|
||||||
|
</label>
|
||||||
<Input
|
<Input
|
||||||
|
id="max-steps"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="40"
|
placeholder="40"
|
||||||
min={1}
|
min={1}
|
||||||
|
|||||||
@@ -71,7 +71,6 @@ export function HistoryDetail({
|
|||||||
{/* Events (read-only) */}
|
{/* Events (read-only) */}
|
||||||
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
<div className="flex-1 overflow-y-auto p-3 space-y-2">
|
||||||
{session.history.map((event, index) => (
|
{session.history.map((event, index) => (
|
||||||
// eslint-disable-next-line react-x/no-array-index-key
|
|
||||||
<EventCard key={index} event={event} />
|
<EventCard key={index} event={event} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import { ArrowDownToLine, ArrowLeft, CheckCircle, RotateCcw, Trash2, XCircle } from 'lucide-react'
|
import {
|
||||||
|
ArrowDownToLine,
|
||||||
|
ArrowLeft,
|
||||||
|
CheckCircle,
|
||||||
|
History,
|
||||||
|
RotateCcw,
|
||||||
|
Trash2,
|
||||||
|
XCircle,
|
||||||
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -29,12 +37,16 @@ export function HistoryList({
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setSessions(await listSessions())
|
try {
|
||||||
setLoading(false)
|
setSessions(await listSessions())
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[HistoryList] Failed to load sessions:', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
load()
|
load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
@@ -58,7 +70,14 @@ export function HistoryList({
|
|||||||
<div className="flex flex-col h-screen bg-background">
|
<div className="flex flex-col h-screen bg-background">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="flex items-center gap-2 border-b px-3 py-2">
|
<header className="flex items-center gap-2 border-b px-3 py-2">
|
||||||
<Button variant="ghost" size="icon-sm" onClick={onBack} className="cursor-pointer">
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
onClick={onBack}
|
||||||
|
className="cursor-pointer"
|
||||||
|
aria-label="Back"
|
||||||
|
title="Back"
|
||||||
|
>
|
||||||
<ArrowLeft className="size-3.5" />
|
<ArrowLeft className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<span className="text-sm font-medium flex-1">History</span>
|
<span className="text-sm font-medium flex-1">History</span>
|
||||||
@@ -81,14 +100,23 @@ export function HistoryList({
|
|||||||
{/* List */}
|
{/* List */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
<div className="flex flex-col" aria-label="Loading history" aria-busy="true">
|
||||||
Loading...
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<div key={i} className="flex items-start gap-2 px-3 py-2.5 border-b">
|
||||||
|
<div className="size-3.5 mt-0.5 rounded-full bg-muted animate-pulse shrink-0" />
|
||||||
|
<div className="flex-1 space-y-1.5">
|
||||||
|
<div className="h-2.5 bg-muted animate-pulse rounded w-3/4" />
|
||||||
|
<div className="h-2 bg-muted animate-pulse rounded w-1/3" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && sessions.length === 0 && (
|
{!loading && sessions.length === 0 && (
|
||||||
<div className="flex items-center justify-center h-32 text-xs text-muted-foreground">
|
<div className="flex flex-col items-center justify-center h-40 gap-2 text-muted-foreground">
|
||||||
No history yet
|
<History className="size-8 opacity-30" />
|
||||||
|
<p className="text-xs">No history yet</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function ResultCard({
|
|||||||
Result: {success ? 'Success' : 'Failed'}
|
Result: {success ? 'Success' : 'Failed'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[11px] text-muted-foreground pl-5 whitespace-pre-wrap">{text}</p>
|
<p className="text-[12px] text-foreground pl-5 whitespace-pre-wrap">{text}</p>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -146,6 +146,7 @@ function CopyButton({ text, label }: { text: string; label: string }) {
|
|||||||
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
function extractPrompt(rawRequest: unknown, role: 'system' | 'user'): string | null {
|
||||||
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
const messages = (rawRequest as { messages?: { role: string; content?: unknown }[] })?.messages
|
||||||
if (!messages) return null
|
if (!messages) return null
|
||||||
|
if (!Array.isArray(messages)) return null
|
||||||
const msg =
|
const msg =
|
||||||
role === 'system'
|
role === 'system'
|
||||||
? messages.find((m) => m.role === role)
|
? messages.find((m) => m.role === role)
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ async function exposeAgentToPage() {
|
|||||||
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
|
let multiPageAgent: InstanceType<typeof MultiPageAgent> | null = null
|
||||||
|
|
||||||
window.addEventListener('message', async (e) => {
|
window.addEventListener('message', async (e) => {
|
||||||
|
if (e.source !== window) return
|
||||||
|
|
||||||
const data = e.data
|
const data = e.data
|
||||||
if (typeof data !== 'object' || data === null) return
|
if (typeof data !== 'object' || data === null) return
|
||||||
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
|
if (data.channel !== 'PAGE_AGENT_EXT_REQUEST') return
|
||||||
@@ -70,11 +72,15 @@ async function exposeAgentToPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { task, config } = payload
|
const { task, config } = payload
|
||||||
|
const { systemInstruction, ...agentConfig } = config
|
||||||
|
|
||||||
// Dispose old instance before creating new one
|
// Dispose old instance before creating new one
|
||||||
multiPageAgent?.dispose()
|
multiPageAgent?.dispose()
|
||||||
|
|
||||||
multiPageAgent = new MultiPageAgent(config)
|
multiPageAgent = new MultiPageAgent({
|
||||||
|
...agentConfig,
|
||||||
|
instructions: systemInstruction ? { system: systemInstruction } : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
// events
|
// events
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{history.map((event, index) => (
|
{history.map((event, index) => (
|
||||||
// eslint-disable-next-line react-x/no-array-index-key
|
|
||||||
<EventCard key={index} event={event} />
|
<EventCard key={index} event={event} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@@ -206,9 +206,9 @@ export function useHubWs(
|
|||||||
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
|
const [wsState, setWsState] = useState<HubWsState>(() => (wsPort ? 'connecting' : 'disconnected'))
|
||||||
const hubWsRef = useRef<HubWs | null>(null)
|
const hubWsRef = useRef<HubWs | null>(null)
|
||||||
|
|
||||||
const latest = useRef({ execute, stop, configure, config })
|
const latestRef = useRef({ execute, stop, configure, config })
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
latest.current = { execute, stop, configure, config }
|
latestRef.current = { execute, stop, configure, config }
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -218,14 +218,14 @@ export function useHubWs(
|
|||||||
Number(wsPort),
|
Number(wsPort),
|
||||||
{
|
{
|
||||||
onExecute: async (task, incomingConfig) => {
|
onExecute: async (task, incomingConfig) => {
|
||||||
const { execute, configure, config } = latest.current
|
const { execute, configure, config } = latestRef.current
|
||||||
if (incomingConfig) {
|
if (incomingConfig) {
|
||||||
await configure({ ...config, ...incomingConfig } as ExtConfig)
|
await configure({ ...config, ...incomingConfig } as ExtConfig)
|
||||||
}
|
}
|
||||||
const result = await execute(task)
|
const result = await execute(task)
|
||||||
return { success: result.success, data: result.data }
|
return { success: result.success, data: result.data }
|
||||||
},
|
},
|
||||||
onStop: () => latest.current.stop(),
|
onStop: () => latestRef.current.stop(),
|
||||||
},
|
},
|
||||||
setWsState
|
setWsState
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ export interface ExecuteConfig {
|
|||||||
model: string
|
model: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global system-level instructions for the agent.
|
||||||
|
* Equivalent to `AgentConfig.instructions.system`.
|
||||||
|
*/
|
||||||
|
systemInstruction?: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to include the initial tab (that holds this main world script) in the task.
|
* Whether to include the initial tab (that holds this main world script) in the task.
|
||||||
* @default true
|
* @default true
|
||||||
@@ -39,6 +45,8 @@ export default defineUnlistedScript(() => {
|
|||||||
|
|
||||||
const promise = new Promise<ExecutionResult>((resolve, reject) => {
|
const promise = new Promise<ExecutionResult>((resolve, reject) => {
|
||||||
function handleMessage(e: MessageEvent) {
|
function handleMessage(e: MessageEvent) {
|
||||||
|
if (e.source !== window) return
|
||||||
|
|
||||||
const data = e.data
|
const data = e.data
|
||||||
if (typeof data !== 'object' || data === null) return
|
if (typeof data !== 'object' || data === null) return
|
||||||
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
|
if (data.channel !== 'PAGE_AGENT_EXT_RESPONSE') return
|
||||||
@@ -89,6 +97,7 @@ export default defineUnlistedScript(() => {
|
|||||||
baseURL: config.baseURL,
|
baseURL: config.baseURL,
|
||||||
model: config.model,
|
model: config.model,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
|
systemInstruction: config.systemInstruction,
|
||||||
includeInitialTab: config.includeInitialTab,
|
includeInitialTab: config.includeInitialTab,
|
||||||
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
|
experimentalIncludeAllTabs: config.experimentalIncludeAllTabs,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ export default function App() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => setView({ name: 'history' })}
|
onClick={() => setView({ name: 'history' })}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
aria-label="History"
|
||||||
|
title="History"
|
||||||
>
|
>
|
||||||
<History className="size-3.5" />
|
<History className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -155,6 +157,8 @@ export default function App() {
|
|||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={() => setView({ name: 'config' })}
|
onClick={() => setView({ name: 'config' })}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
|
aria-label="Settings"
|
||||||
|
title="Settings"
|
||||||
>
|
>
|
||||||
<Settings className="size-3.5" />
|
<Settings className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -178,7 +182,6 @@ export default function App() {
|
|||||||
{showEmptyState && <EmptyState />}
|
{showEmptyState && <EmptyState />}
|
||||||
|
|
||||||
{history.map((event, index) => (
|
{history.map((event, index) => (
|
||||||
// eslint-disable-next-line react-x/no-array-index-key
|
|
||||||
<EventCard key={index} event={event} />
|
<EventCard key={index} event={event} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -206,6 +209,8 @@ export default function App() {
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={handleStop}
|
onClick={handleStop}
|
||||||
className="size-7"
|
className="size-7"
|
||||||
|
aria-label="Stop task"
|
||||||
|
title="Stop task"
|
||||||
>
|
>
|
||||||
<Square className="size-3" />
|
<Square className="size-3" />
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
@@ -216,6 +221,8 @@ export default function App() {
|
|||||||
onClick={() => handleSubmit()}
|
onClick={() => handleSubmit()}
|
||||||
disabled={!inputValue.trim()}
|
disabled={!inputValue.trim()}
|
||||||
className="size-7 cursor-pointer"
|
className="size-7 cursor-pointer"
|
||||||
|
aria-label="Send"
|
||||||
|
title="Send"
|
||||||
>
|
>
|
||||||
<Send className="size-3" />
|
<Send className="size-3" />
|
||||||
</InputGroupButton>
|
</InputGroupButton>
|
||||||
|
|||||||
@@ -3,26 +3,11 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"types": ["node", "chrome"],
|
||||||
"paths": {
|
"paths": {
|
||||||
// Self root
|
"@/*": ["./src/*"]
|
||||||
"@/*": ["src/*"],
|
|
||||||
|
|
||||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
|
||||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
|
||||||
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
|
|
||||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
"references": [
|
|
||||||
//
|
|
||||||
{ "path": "../llms" },
|
|
||||||
{ "path": "../page-controller" },
|
|
||||||
{ "path": "../core" },
|
|
||||||
{ "path": "../ui" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-10
@@ -1,15 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/llms",
|
"name": "@page-agent/llms",
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-llms.js",
|
"main": "./src/index.ts",
|
||||||
"module": "./dist/lib/page-agent-llms.js",
|
"types": "./src/index.ts",
|
||||||
"types": "./dist/lib/index.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/lib/index.d.ts",
|
"types": "./src/index.ts",
|
||||||
"import": "./dist/lib/page-agent-llms.js",
|
"default": "./src/index.ts"
|
||||||
"default": "./dist/lib/page-agent-llms.js"
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/lib/page-agent-llms.js",
|
||||||
|
"module": "./dist/lib/page-agent-llms.js",
|
||||||
|
"types": "./dist/lib/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/lib/index.d.ts",
|
||||||
|
"import": "./dist/lib/page-agent-llms.js",
|
||||||
|
"default": "./dist/lib/page-agent-llms.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -33,8 +43,8 @@
|
|||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
||||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
@@ -43,6 +53,6 @@
|
|||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
import { modelPatch, zodToOpenAITool } from './utils'
|
import { modelPatch, zodToOpenAITool } from './utils'
|
||||||
|
|
||||||
@@ -45,6 +45,17 @@ export class OpenAIClient implements LLMClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modelPatch(requestBody)
|
modelPatch(requestBody)
|
||||||
|
let transformedBody: Record<string, unknown> | undefined
|
||||||
|
try {
|
||||||
|
transformedBody = this.config.transformRequestBody(requestBody)
|
||||||
|
} catch (error) {
|
||||||
|
throw new InvokeError(
|
||||||
|
InvokeErrorTypes.CONFIG_ERROR,
|
||||||
|
`transformRequestBody failed: ${(error as Error).message}`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const finalRequestBody = transformedBody ?? requestBody
|
||||||
|
|
||||||
// 2. Call API
|
// 2. Call API
|
||||||
let response: Response
|
let response: Response
|
||||||
@@ -55,14 +66,14 @@ export class OpenAIClient implements LLMClient {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
...(this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }),
|
||||||
},
|
},
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(finalRequestBody),
|
||||||
signal: abortSignal,
|
signal: abortSignal,
|
||||||
})
|
})
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const isAbortError = (error as any)?.name === 'AbortError'
|
const isAbortError = (error as any)?.name === 'AbortError'
|
||||||
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
|
const errorMessage = isAbortError ? 'Network request aborted' : 'Network request failed'
|
||||||
if (!isAbortError) console.error(error)
|
if (!isAbortError) console.error(error)
|
||||||
throw new InvokeError(InvokeErrorType.NETWORK_ERROR, errorMessage, error)
|
throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, errorMessage, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Handle HTTP errors
|
// 3. Handle HTTP errors
|
||||||
@@ -73,27 +84,27 @@ export class OpenAIClient implements LLMClient {
|
|||||||
|
|
||||||
if (response.status === 401 || response.status === 403) {
|
if (response.status === 401 || response.status === 403) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.AUTH_ERROR,
|
InvokeErrorTypes.AUTH_ERROR,
|
||||||
`Authentication failed: ${errorMessage}`,
|
`Authentication failed: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.RATE_LIMIT,
|
InvokeErrorTypes.RATE_LIMIT,
|
||||||
`Rate limit exceeded: ${errorMessage}`,
|
`Rate limit exceeded: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (response.status >= 500) {
|
if (response.status >= 500) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.SERVER_ERROR,
|
InvokeErrorTypes.SERVER_ERROR,
|
||||||
`Server error: ${errorMessage}`,
|
`Server error: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`HTTP ${response.status}: ${errorMessage}`,
|
`HTTP ${response.status}: ${errorMessage}`,
|
||||||
errorData
|
errorData
|
||||||
)
|
)
|
||||||
@@ -104,7 +115,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
|
|
||||||
const choice = data.choices?.[0]
|
const choice = data.choices?.[0]
|
||||||
if (!choice) {
|
if (!choice) {
|
||||||
throw new InvokeError(InvokeErrorType.UNKNOWN, 'No choices in response', data)
|
throw new InvokeError(InvokeErrorTypes.UNKNOWN, 'No choices in response', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check finish_reason
|
// Check finish_reason
|
||||||
@@ -115,21 +126,21 @@ export class OpenAIClient implements LLMClient {
|
|||||||
break
|
break
|
||||||
case 'length':
|
case 'length':
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.CONTEXT_LENGTH,
|
InvokeErrorTypes.CONTEXT_LENGTH,
|
||||||
'Response truncated: max tokens reached',
|
'Response truncated: max tokens reached',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
case 'content_filter':
|
case 'content_filter':
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.CONTENT_FILTER,
|
InvokeErrorTypes.CONTENT_FILTER,
|
||||||
'Content filtered by safety system',
|
'Content filtered by safety system',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`Unexpected finish_reason: ${choice.finish_reason}`,
|
`Unexpected finish_reason: ${choice.finish_reason}`,
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -144,7 +155,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
|
const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name
|
||||||
if (!toolCallName) {
|
if (!toolCallName) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.NO_TOOL_CALL,
|
InvokeErrorTypes.NO_TOOL_CALL,
|
||||||
'No tool call found in response',
|
'No tool call found in response',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -154,7 +165,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const tool = tools[toolCallName]
|
const tool = tools[toolCallName]
|
||||||
if (!tool) {
|
if (!tool) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
`Tool "${toolCallName}" not found in tools`,
|
`Tool "${toolCallName}" not found in tools`,
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -165,7 +176,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
|
const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments
|
||||||
if (!argString) {
|
if (!argString) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'No tool call arguments found',
|
'No tool call arguments found',
|
||||||
undefined,
|
undefined,
|
||||||
data
|
data
|
||||||
@@ -177,7 +188,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
parsedArgs = JSON.parse(argString)
|
parsedArgs = JSON.parse(argString)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'Failed to parse tool arguments as JSON',
|
'Failed to parse tool arguments as JSON',
|
||||||
error,
|
error,
|
||||||
data
|
data
|
||||||
@@ -189,7 +200,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
if (!validation.success) {
|
if (!validation.success) {
|
||||||
console.error(z.prettifyError(validation.error))
|
console.error(z.prettifyError(validation.error))
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
'Tool arguments validation failed',
|
'Tool arguments validation failed',
|
||||||
validation.error,
|
validation.error,
|
||||||
data
|
data
|
||||||
@@ -203,7 +214,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
toolResult = await tool.execute(toolInput)
|
toolResult = await tool.execute(toolInput)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new InvokeError(
|
throw new InvokeError(
|
||||||
InvokeErrorType.TOOL_EXECUTION_ERROR,
|
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
||||||
`Tool execution failed: ${(e as Error).message}`,
|
`Tool execution failed: ${(e as Error).message}`,
|
||||||
e,
|
e,
|
||||||
data
|
data
|
||||||
@@ -225,7 +236,7 @@ export class OpenAIClient implements LLMClient {
|
|||||||
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens,
|
||||||
},
|
},
|
||||||
rawResponse: data,
|
rawResponse: data,
|
||||||
rawRequest: requestBody,
|
rawRequest: finalRequestBody,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Error types and error handling for LLM invocations
|
* Error types and error handling for LLM invocations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const InvokeErrorType = {
|
export const InvokeErrorTypes = {
|
||||||
// Retryable
|
// Retryable
|
||||||
NETWORK_ERROR: 'network_error', // Network error, retry
|
NETWORK_ERROR: 'network_error', // Network error, retry
|
||||||
RATE_LIMIT: 'rate_limit', // Rate limit, retry
|
RATE_LIMIT: 'rate_limit', // Rate limit, retry
|
||||||
@@ -14,12 +14,13 @@ export const InvokeErrorType = {
|
|||||||
UNKNOWN: 'unknown',
|
UNKNOWN: 'unknown',
|
||||||
|
|
||||||
// Non-retryable
|
// Non-retryable
|
||||||
|
CONFIG_ERROR: 'config_error', // Invalid local configuration or hook
|
||||||
AUTH_ERROR: 'auth_error', // Authentication failed
|
AUTH_ERROR: 'auth_error', // Authentication failed
|
||||||
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
CONTEXT_LENGTH: 'context_length', // Prompt too long
|
||||||
CONTENT_FILTER: 'content_filter', // Content filtered
|
CONTENT_FILTER: 'content_filter', // Content filtered
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type InvokeErrorType = (typeof InvokeErrorType)[keyof typeof InvokeErrorType]
|
type InvokeErrorType = (typeof InvokeErrorTypes)[keyof typeof InvokeErrorTypes]
|
||||||
|
|
||||||
export class InvokeError extends Error {
|
export class InvokeError extends Error {
|
||||||
type: InvokeErrorType
|
type: InvokeErrorType
|
||||||
@@ -44,13 +45,13 @@ export class InvokeError extends Error {
|
|||||||
if (isAbortError) return false
|
if (isAbortError) return false
|
||||||
|
|
||||||
const retryableTypes: InvokeErrorType[] = [
|
const retryableTypes: InvokeErrorType[] = [
|
||||||
InvokeErrorType.NETWORK_ERROR,
|
InvokeErrorTypes.NETWORK_ERROR,
|
||||||
InvokeErrorType.RATE_LIMIT,
|
InvokeErrorTypes.RATE_LIMIT,
|
||||||
InvokeErrorType.SERVER_ERROR,
|
InvokeErrorTypes.SERVER_ERROR,
|
||||||
InvokeErrorType.NO_TOOL_CALL,
|
InvokeErrorTypes.NO_TOOL_CALL,
|
||||||
InvokeErrorType.INVALID_TOOL_ARGS,
|
InvokeErrorTypes.INVALID_TOOL_ARGS,
|
||||||
InvokeErrorType.TOOL_EXECUTION_ERROR,
|
InvokeErrorTypes.TOOL_EXECUTION_ERROR,
|
||||||
InvokeErrorType.UNKNOWN,
|
InvokeErrorTypes.UNKNOWN,
|
||||||
]
|
]
|
||||||
return retryableTypes.includes(type)
|
return retryableTypes.includes(type)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { OpenAIClient } from './OpenAIClient'
|
import { OpenAIClient } from './OpenAIClient'
|
||||||
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
import { DEFAULT_TEMPERATURE, LLM_MAX_RETRIES } from './constants'
|
||||||
import { InvokeError, InvokeErrorType } from './errors'
|
import { InvokeError, InvokeErrorTypes } from './errors'
|
||||||
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
import type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool } from './types'
|
||||||
|
|
||||||
export { InvokeError, InvokeErrorType }
|
export { InvokeError, InvokeErrorTypes }
|
||||||
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
export type { InvokeOptions, InvokeResult, LLMClient, LLMConfig, Message, Tool }
|
||||||
|
|
||||||
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
||||||
@@ -21,6 +21,7 @@ export function parseLLMConfig(config: LLMConfig): Required<LLMConfig> {
|
|||||||
apiKey: config.apiKey || '',
|
apiKey: config.apiKey || '',
|
||||||
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
temperature: config.temperature ?? DEFAULT_TEMPERATURE,
|
||||||
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
maxRetries: config.maxRetries ?? LLM_MAX_RETRIES,
|
||||||
|
transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
|
||||||
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
disableNamedToolChoice: config.disableNamedToolChoice ?? false,
|
||||||
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
customFetch: (config.customFetch ?? fetch).bind(globalThis), // fetch will be illegal unless bound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,16 @@ export interface LLMConfig {
|
|||||||
temperature?: number
|
temperature?: number
|
||||||
maxRetries?: number
|
maxRetries?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the final request body before sending it to the provider.
|
||||||
|
* Use this to implement provider-specific request tweaks such as caching hints or custom flags.
|
||||||
|
*
|
||||||
|
* Return a new object, or mutate the input object and return undefined.
|
||||||
|
*/
|
||||||
|
transformRequestBody?: (
|
||||||
|
requestBody: Record<string, unknown>
|
||||||
|
) => Record<string, unknown> | undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove the tool_choice field from the request.
|
* remove the tool_choice field from the request.
|
||||||
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
* @note fix "Invalid tool_choice type: 'object'" for some LLMs.
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
debug('Applying Claude patch: convert tool_choice format')
|
debug('Applying Claude patch: convert tool_choice format')
|
||||||
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
|
body.tool_choice = { type: 'tool', name: body.tool_choice.function.name }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Claude naming pattern has changed
|
||||||
|
// needs proper handling
|
||||||
|
if (modelName.startsWith('claude-opus-4-7') || modelName.startsWith('claude-opus-47')) {
|
||||||
|
debug('Applying Claude-4.7 patch: remove temperature')
|
||||||
|
delete body.temperature
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('grok')) {
|
if (modelName.startsWith('grok')) {
|
||||||
@@ -74,10 +81,12 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
debug('Applying GPT-51 patch: disable reasoning')
|
debug('Applying GPT-51 patch: disable reasoning')
|
||||||
body.reasoning_effort = 'none'
|
body.reasoning_effort = 'none'
|
||||||
} else if (modelName.startsWith('gpt-54')) {
|
} else if (modelName.startsWith('gpt-54')) {
|
||||||
debug(
|
debug('Applying GPT-5.4 patch: remove reasoning_effort')
|
||||||
'Applying GPT-5.4 patch: skip reasoning_effort because chat/completions rejects it with function tools'
|
|
||||||
)
|
|
||||||
delete body.reasoning_effort
|
delete body.reasoning_effort
|
||||||
|
} else if (modelName.startsWith('gpt-55')) {
|
||||||
|
debug('Applying GPT-5.4 patch: remove reasoning_effort and temperature')
|
||||||
|
delete body.reasoning_effort
|
||||||
|
delete body.temperature
|
||||||
} else if (modelName.startsWith('gpt-5-mini')) {
|
} else if (modelName.startsWith('gpt-5-mini')) {
|
||||||
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
debug('Applying GPT-5-mini patch: set reasoning effort to low, temperature to 1')
|
||||||
body.reasoning_effort = 'low'
|
body.reasoning_effort = 'low'
|
||||||
@@ -93,6 +102,11 @@ export function modelPatch(body: Record<string, any>) {
|
|||||||
body.reasoning_effort = 'minimal'
|
body.reasoning_effort = 'minimal'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modelName.startsWith('deepseek')) {
|
||||||
|
debug('Applying DeepSeek patch: remove tool_choice')
|
||||||
|
delete body.tool_choice
|
||||||
|
}
|
||||||
|
|
||||||
if (modelName.startsWith('minimax')) {
|
if (modelName.startsWith('minimax')) {
|
||||||
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
debug('Applying MiniMax patch: clamp temperature to (0, 1]')
|
||||||
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
// MiniMax API rejects temperature = 0; clamp to a small positive value
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
// @workaround DTS bug
|
|
||||||
// dts do not work with monorepo path mapping
|
|
||||||
// disable path mapping for it
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
|
|||||||
@@ -11,11 +11,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/llms`))
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true })],
|
plugins: [
|
||||||
|
dts({
|
||||||
|
bundleTypes: true,
|
||||||
|
compilerOptions: {
|
||||||
|
composite: true,
|
||||||
|
noEmit: false,
|
||||||
|
emitDeclarationOnly: true,
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/index.ts'),
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
|
|||||||
@@ -36,11 +36,11 @@ Same format — add the config to the MCP settings of your client.
|
|||||||
|
|
||||||
## MCP Tools
|
## MCP Tools
|
||||||
|
|
||||||
| Tool | Input | Description |
|
| Tool | Input | Description |
|
||||||
| -------------- | ------------------ | ---------------------------------------------------- |
|
| -------------- | ------------------ | ----------------------------------------------------- |
|
||||||
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
| `execute_task` | `{ task: string }` | Execute a browser task in natural language. Blocking. |
|
||||||
| `get_status` | — | Returns `{ connected, busy }` |
|
| `get_status` | — | Returns `{ connected, busy }` |
|
||||||
| `stop_task` | — | Stop the currently running task. |
|
| `stop_task` | — | Stop the currently running task. |
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -88,7 +88,6 @@ src/
|
|||||||
## Dev
|
## Dev
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build:libs
|
|
||||||
npm run dev:ext
|
npm run dev:ext
|
||||||
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
npx @modelcontextprotocol/inspector node packages/mcp/src/index.js
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/mcp",
|
"name": "@page-agent/mcp",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"page-agent-mcp": "src/index.js"
|
"page-agent-mcp": "src/index.js"
|
||||||
@@ -30,6 +30,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.29.0",
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"ws": "^8.20.0",
|
"ws": "^8.20.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { WebSocketServer } from 'ws'
|
|||||||
|
|
||||||
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
|
const EXT_ID = 'akldabonmimlicnjlflnapfeklbfemhj'
|
||||||
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
|
const STORE_URL = `https://chromewebstore.google.com/detail/page-agent-ext/${EXT_ID}`
|
||||||
|
const LOOPBACK_HOST = 'localhost'
|
||||||
|
|
||||||
const launcherTemplate = readFileSync(
|
const launcherTemplate = readFileSync(
|
||||||
fileURLToPath(new URL('./launcher.html', import.meta.url)),
|
fileURLToPath(new URL('./launcher.html', import.meta.url)),
|
||||||
@@ -60,8 +61,8 @@ export class HubBridge {
|
|||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.#httpServer.listen(this.port, () => {
|
this.#httpServer.listen(this.port, LOOPBACK_HOST, () => {
|
||||||
console.error(`[page-agent-mcp] HTTP + WS on http://localhost:${this.port}`)
|
console.error(`[page-agent-mcp] HTTP + WS on http://${LOOPBACK_HOST}:${this.port}`)
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||||
import { exec } from 'node:child_process'
|
import { exec } from 'node:child_process'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
import { platform } from 'node:os'
|
import { platform } from 'node:os'
|
||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ import { HubBridge } from './hub-bridge.js'
|
|||||||
|
|
||||||
const env = process.env
|
const env = process.env
|
||||||
const port = parseInt(env.PORT || '38401')
|
const port = parseInt(env.PORT || '38401')
|
||||||
|
const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
/** @type {Record<string, string>} */
|
||||||
const llmConfig = {}
|
const llmConfig = {}
|
||||||
@@ -30,7 +32,7 @@ exec(`${cmd} "${url}"`, (err) => {
|
|||||||
|
|
||||||
// --- MCP server (stdio) ---
|
// --- MCP server (stdio) ---
|
||||||
|
|
||||||
const mcpServer = new McpServer({ name: 'page-agent', version: '1.5.8' })
|
const mcpServer = new McpServer({ name: 'page-agent', version })
|
||||||
|
|
||||||
mcpServer.registerTool(
|
mcpServer.registerTool(
|
||||||
'execute_task',
|
'execute_task',
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png" />
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="https://img.alicdn.com/imgextra/i1/O1CN01mRGret1QrKiu7CFJI_!!6000000002029-2-tps-64-64.png"
|
||||||
|
/>
|
||||||
<title>Page Agent MCP Launcher</title>
|
<title>Page Agent MCP Launcher</title>
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
@@ -172,16 +175,26 @@
|
|||||||
If the extension is outdated, please update it to the latest version.
|
If the extension is outdated, please update it to the latest version.
|
||||||
</li>
|
</li>
|
||||||
<li data-i18n="tip_other_browser">
|
<li data-i18n="tip_other_browser">
|
||||||
If the extension is not installed in this browser, open this page from the
|
If the extension is not installed in this browser, open this page from the browser that
|
||||||
browser that has it installed.
|
has it installed.
|
||||||
</li>
|
</li>
|
||||||
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
|
<li data-i18n="tip_refresh">Refresh this page after installing or updating.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="links">
|
<div class="links">
|
||||||
<a href="https://alibaba.github.io/page-agent/docs/introduction/overview" target="_blank" data-i18n="link_docs">Docs</a>
|
<a
|
||||||
<a href="https://github.com/alibaba/page-agent/issues" target="_blank" data-i18n="link_issues">Report an Issue</a>
|
href="https://alibaba.github.io/page-agent/docs/introduction/overview"
|
||||||
|
target="_blank"
|
||||||
|
data-i18n="link_docs"
|
||||||
|
>Docs</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="https://github.com/alibaba/page-agent/issues"
|
||||||
|
target="_blank"
|
||||||
|
data-i18n="link_issues"
|
||||||
|
>Report an Issue</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -197,8 +210,7 @@
|
|||||||
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
|
install_sub: 'Page Agent 需要安装最新版浏览器插件才能运行。',
|
||||||
install_btn: '从 Chrome 应用商店安装',
|
install_btn: '从 Chrome 应用商店安装',
|
||||||
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
|
tip_outdated: '如果插件版本过旧,请更新到最新版本。',
|
||||||
tip_other_browser:
|
tip_other_browser: '如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
|
||||||
'如果该浏览器中未安装插件,请从装有插件的浏览器打开此页面。',
|
|
||||||
tip_refresh: '安装或更新后,请刷新此页面。',
|
tip_refresh: '安装或更新后,请刷新此页面。',
|
||||||
link_docs: '文档',
|
link_docs: '文档',
|
||||||
link_issues: '问题反馈',
|
link_issues: '问题反馈',
|
||||||
@@ -220,13 +232,9 @@
|
|||||||
if (!globalThis.chrome?.runtime?.sendMessage) {
|
if (!globalThis.chrome?.runtime?.sendMessage) {
|
||||||
showInstall()
|
showInstall()
|
||||||
} else {
|
} else {
|
||||||
chrome.runtime.sendMessage(
|
chrome.runtime.sendMessage(EXT_ID, { type: 'OPEN_HUB', wsPort }, (response) => {
|
||||||
EXT_ID,
|
if (chrome.runtime.lastError || !response?.ok) showInstall()
|
||||||
{ type: 'OPEN_HUB', wsPort },
|
})
|
||||||
(response) => {
|
|
||||||
if (chrome.runtime.lastError || !response?.ok) showInstall()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showInstall()
|
showInstall()
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "page-agent",
|
"name": "page-agent",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/esm/page-agent.js",
|
"main": "./src/PageAgent.ts",
|
||||||
"module": "./dist/esm/page-agent.js",
|
"types": "./src/PageAgent.ts",
|
||||||
"types": "./dist/esm/PageAgent.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/esm/PageAgent.d.ts",
|
"types": "./src/PageAgent.ts",
|
||||||
"import": "./dist/esm/page-agent.js",
|
"default": "./src/PageAgent.ts"
|
||||||
"default": "./dist/esm/page-agent.js"
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/esm/page-agent.js",
|
||||||
|
"module": "./dist/esm/page-agent.js",
|
||||||
|
"types": "./dist/esm/PageAgent.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/PageAgent.d.ts",
|
||||||
|
"import": "./dist/esm/page-agent.js",
|
||||||
|
"default": "./dist/esm/page-agent.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -40,20 +50,20 @@
|
|||||||
"build": "vite build && npm run build:demo",
|
"build": "vite build && npm run build:demo",
|
||||||
"build:demo": "vite build --config vite.iife.config.js",
|
"build:demo": "vite build --config vite.iife.config.js",
|
||||||
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
"dev:demo": "concurrently \"vite build --config vite.iife.config.js --watch\" \"npx serve dist/iife -p 5174\"",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['README.md','LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
||||||
"postpublish": "node -e \"['README.md','LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@page-agent/core": "1.7.0",
|
"@page-agent/core": "1.8.2",
|
||||||
"@page-agent/llms": "1.7.0",
|
"@page-agent/llms": "1.8.2",
|
||||||
"@page-agent/page-controller": "1.7.0",
|
"@page-agent/page-controller": "1.8.2",
|
||||||
"@page-agent/ui": "1.7.0",
|
"@page-agent/ui": "1.8.2",
|
||||||
"chalk": "^5.6.2"
|
"chalk": "^5.6.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@
|
|||||||
*/
|
*/
|
||||||
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
import { PageAgent, type PageAgentConfig } from './PageAgent'
|
||||||
|
|
||||||
|
const currentScript = document.currentScript as HTMLScriptElement | null
|
||||||
|
const currentScriptURL = currentScript?.src ? new URL(currentScript.src) : null
|
||||||
|
const autoInit = currentScriptURL?.searchParams.get('autoInit') !== 'false'
|
||||||
|
|
||||||
// Clean up existing instances to prevent multiple injections from bookmarklet
|
// Clean up existing instances to prevent multiple injections from bookmarklet
|
||||||
if (window.pageAgent) {
|
if (autoInit && window.pageAgent) {
|
||||||
window.pageAgent.dispose()
|
window.pageAgent.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,30 +22,34 @@ const DEMO_BASE_URL = 'https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run'
|
|||||||
const DEMO_API_KEY = 'NA'
|
const DEMO_API_KEY = 'NA'
|
||||||
|
|
||||||
// in case document.x is not ready yet
|
// in case document.x is not ready yet
|
||||||
setTimeout(() => {
|
if (autoInit) {
|
||||||
const currentScript = document.currentScript as HTMLScriptElement | null
|
setTimeout(() => {
|
||||||
let config: PageAgentConfig
|
let config: PageAgentConfig
|
||||||
|
let showPanel = true
|
||||||
|
|
||||||
if (currentScript) {
|
if (currentScriptURL) {
|
||||||
console.log('🚀 page-agent.js detected current script:', currentScript.src)
|
const url = currentScriptURL
|
||||||
const url = new URL(currentScript.src)
|
const model = url.searchParams.get('model') || DEMO_MODEL
|
||||||
const model = url.searchParams.get('model') || DEMO_MODEL
|
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
||||||
const baseURL = url.searchParams.get('baseURL') || DEMO_BASE_URL
|
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
||||||
const apiKey = url.searchParams.get('apiKey') || DEMO_API_KEY
|
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
||||||
const language = (url.searchParams.get('lang') as 'zh-CN' | 'en-US') || 'zh-CN'
|
showPanel = ((url.searchParams.get('showPanel') as 'true' | 'false') || 'true') === 'true'
|
||||||
config = { model, baseURL, apiKey, language }
|
config = { model, baseURL, apiKey, language }
|
||||||
} else {
|
} else {
|
||||||
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
console.log('🚀 page-agent.js no current script detected, using default demo config')
|
||||||
config = {
|
config = {
|
||||||
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
|
model: import.meta.env.LLM_MODEL_NAME ? import.meta.env.LLM_MODEL_NAME : DEMO_MODEL,
|
||||||
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
|
baseURL: import.meta.env.LLM_BASE_URL ? import.meta.env.LLM_BASE_URL : DEMO_BASE_URL,
|
||||||
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
|
apiKey: import.meta.env.LLM_API_KEY ? import.meta.env.LLM_API_KEY : DEMO_API_KEY,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Create agent
|
// Create agent
|
||||||
window.pageAgent = new PageAgent(config)
|
window.pageAgent = new PageAgent(config)
|
||||||
window.pageAgent.panel.show()
|
if (showPanel) {
|
||||||
|
window.pageAgent.panel.show()
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
console.log('🚀 page-agent.js initialized with config:', window.pageAgent.config)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
// @workaround DTS bug
|
|
||||||
// dts do not work with monorepo path mapping
|
|
||||||
// disable path mapping for it
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist",
|
|
||||||
"paths": {
|
|
||||||
//
|
|
||||||
"@page-agent/llms": ["../llms/src/index.ts"],
|
|
||||||
"@page-agent/page-controller": ["../page-controller/src/PageController.ts"],
|
|
||||||
"@page-agent/core": ["../core/src/PageAgentCore.ts"],
|
|
||||||
"@page-agent/ui": ["../ui/src/index.ts"]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts"],
|
"include": ["src/**/*.ts"],
|
||||||
"exclude": ["dist", "node_modules"],
|
"exclude": ["dist", "node_modules"]
|
||||||
"references": [
|
|
||||||
//
|
|
||||||
{ "path": "../llms" },
|
|
||||||
{ "path": "../page-controller" },
|
|
||||||
{ "path": "../core" },
|
|
||||||
{ "path": "../ui" }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,18 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
dts({
|
||||||
|
bundleTypes: true,
|
||||||
|
compilerOptions: {
|
||||||
|
composite: true,
|
||||||
|
noEmit: false,
|
||||||
|
emitDeclarationOnly: true,
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
],
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/PageAgent.ts'),
|
entry: resolve(__dirname, 'src/PageAgent.ts'),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { config as dotenvConfig } from 'dotenv'
|
|||||||
import { dirname, resolve } from 'path'
|
import { dirname, resolve } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
// import { analyzer } from 'vite-bundle-analyzer'
|
|
||||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
@@ -21,17 +20,6 @@ export default defineConfig(() => ({
|
|||||||
// analyzer()
|
// analyzer()
|
||||||
],
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@page-agent/page-controller': resolve(__dirname, '../page-controller/src/PageController.ts'),
|
|
||||||
'@page-agent/llms': resolve(__dirname, '../llms/src/index.ts'),
|
|
||||||
'@page-agent/core': resolve(__dirname, '../core/src/PageAgentCore.ts'),
|
|
||||||
'@page-agent/ui': resolve(__dirname, '../ui/src/index.ts'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/demo.ts'),
|
entry: resolve(__dirname, 'src/demo.ts'),
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/page-controller",
|
"name": "@page-agent/page-controller",
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-controller.js",
|
"main": "./src/PageController.ts",
|
||||||
"module": "./dist/lib/page-controller.js",
|
"types": "./src/PageController.ts",
|
||||||
"types": "./dist/lib/PageController.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/lib/PageController.d.ts",
|
"types": "./src/PageController.ts",
|
||||||
"import": "./dist/lib/page-controller.js",
|
"default": "./src/PageController.ts"
|
||||||
"default": "./dist/lib/page-controller.js"
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/lib/page-controller.js",
|
||||||
|
"module": "./dist/lib/page-controller.js",
|
||||||
|
"types": "./dist/lib/PageController.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/lib/PageController.d.ts",
|
||||||
|
"import": "./dist/lib/page-controller.js",
|
||||||
|
"default": "./dist/lib/page-controller.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -32,8 +42,8 @@
|
|||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
||||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ai-motion": "^0.4.8"
|
"ai-motion": "^0.4.8"
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export class PageController extends EventTarget {
|
|||||||
|
|
||||||
const blacklist = [
|
const blacklist = [
|
||||||
...(this.config.interactiveBlacklist || []),
|
...(this.config.interactiveBlacklist || []),
|
||||||
...document.querySelectorAll('[data-page-agent-not-interactive]').values(),
|
...Array.from(document.querySelectorAll('[data-page-agent-not-interactive]')),
|
||||||
]
|
]
|
||||||
|
|
||||||
this.flatTree = dom.getFlatTree({
|
this.flatTree = dom.getFlatTree({
|
||||||
@@ -193,7 +193,11 @@ export class PageController extends EventTarget {
|
|||||||
interactiveBlacklist: blacklist,
|
interactiveBlacklist: blacklist,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.simplifiedHTML = dom.flatTreeToString(this.flatTree, this.config.includeAttributes)
|
this.simplifiedHTML = dom.flatTreeToString(
|
||||||
|
this.flatTree,
|
||||||
|
this.config.includeAttributes,
|
||||||
|
this.config.keepSemanticTags
|
||||||
|
)
|
||||||
|
|
||||||
this.selectorMap.clear()
|
this.selectorMap.clear()
|
||||||
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
this.selectorMap = dom.getSelectorMap(this.flatTree)
|
||||||
@@ -322,11 +326,11 @@ export class PageController extends EventTarget {
|
|||||||
|
|
||||||
this.assertIndexed()
|
this.assertIndexed()
|
||||||
|
|
||||||
const scrollAmount = pixels ?? numPages * (down ? 1 : -1) * window.innerHeight
|
const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1)
|
||||||
|
|
||||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||||
|
|
||||||
const message = await scrollVertically(down, scrollAmount, element)
|
const message = await scrollVertically(scrollAmount, element)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -357,7 +361,7 @@ export class PageController extends EventTarget {
|
|||||||
|
|
||||||
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
const element = index !== undefined ? getElementByIndex(this.selectorMap, index) : null
|
||||||
|
|
||||||
const message = await scrollHorizontally(right, scrollAmount, element)
|
const message = await scrollHorizontally(scrollAmount, element)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -274,14 +274,7 @@ export async function scrollIntoViewIfNeeded(element: Element) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function scrollVertically(scroll_amount: number, element?: HTMLElement | null) {
|
||||||
* @private Internal method, subject to change at any time.
|
|
||||||
*/
|
|
||||||
export async function scrollVertically(
|
|
||||||
down: boolean,
|
|
||||||
scroll_amount: number,
|
|
||||||
element?: HTMLElement | null
|
|
||||||
) {
|
|
||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
@@ -294,7 +287,10 @@ export async function scrollVertically(
|
|||||||
|
|
||||||
while (currentElement && attempts < 10) {
|
while (currentElement && attempts < 10) {
|
||||||
const computedStyle = window.getComputedStyle(currentElement)
|
const computedStyle = window.getComputedStyle(currentElement)
|
||||||
const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY)
|
const hasScrollableY =
|
||||||
|
/(auto|scroll|overlay)/.test(computedStyle.overflowY) ||
|
||||||
|
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
|
||||||
|
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
|
||||||
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
|
||||||
|
|
||||||
if (hasScrollableY && canScrollVertically) {
|
if (hasScrollableY && canScrollVertically) {
|
||||||
@@ -346,9 +342,20 @@ export async function scrollVertically(
|
|||||||
el.scrollHeight > el.clientHeight &&
|
el.scrollHeight > el.clientHeight &&
|
||||||
bigEnough(el)
|
bigEnough(el)
|
||||||
|
|
||||||
|
// @deprecated Heuristic container search.
|
||||||
|
// Unreliable in multi-panel layouts. Should guide LLMs to use indexed scroll for consistency.
|
||||||
|
// TODO: remove this fallback
|
||||||
|
|
||||||
|
// try to find the nearest scrollable container
|
||||||
|
// document.activeElement is usually body.
|
||||||
|
// After a successful element.focus(), activeElement become the nearest focusable parent
|
||||||
|
|
||||||
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
||||||
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
||||||
|
|
||||||
|
// Something is wrong if it falls back to global '*' search
|
||||||
|
// TODO: Return error message instead of global '*' search
|
||||||
|
|
||||||
el = canScroll(el)
|
el = canScroll(el)
|
||||||
? el
|
? el
|
||||||
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
|
: Array.from(document.querySelectorAll<HTMLElement>('*')).find(canScroll) ||
|
||||||
@@ -379,6 +386,10 @@ export async function scrollVertically(
|
|||||||
return `✅ Scrolled page by ${scrolled}px.`
|
return `✅ Scrolled page by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
// Container scroll
|
// Container scroll
|
||||||
|
|
||||||
|
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
|
||||||
|
console.log(`[PageController] ${warningMsg}`)
|
||||||
|
|
||||||
const scrollBefore = el!.scrollTop
|
const scrollBefore = el!.scrollTop
|
||||||
const scrollMax = el!.scrollHeight - el!.clientHeight
|
const scrollMax = el!.scrollHeight - el!.clientHeight
|
||||||
|
|
||||||
@@ -390,29 +401,22 @@ export async function scrollVertically(
|
|||||||
|
|
||||||
if (Math.abs(scrolled) < 1) {
|
if (Math.abs(scrolled) < 1) {
|
||||||
return dy > 0
|
return dy > 0
|
||||||
? `⚠️ Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
|
? `⚠️ ${warningMsg} Already at the bottom of container (${el!.tagName}), cannot scroll down further.`
|
||||||
: `⚠️ Already at the top of container (${el!.tagName}), cannot scroll up further.`
|
: `⚠️ ${warningMsg} Already at the top of container (${el!.tagName}), cannot scroll up further.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
|
||||||
const reachedTop = dy < 0 && scrollAfter <= 1
|
const reachedTop = dy < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
if (reachedBottom)
|
if (reachedBottom)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the bottom.`
|
||||||
if (reachedTop)
|
if (reachedTop)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the top.`
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function scrollHorizontally(scroll_amount: number, element?: HTMLElement | null) {
|
||||||
* @private Internal method, subject to change at any time.
|
|
||||||
*/
|
|
||||||
export async function scrollHorizontally(
|
|
||||||
right: boolean,
|
|
||||||
scroll_amount: number,
|
|
||||||
element?: HTMLElement | null
|
|
||||||
) {
|
|
||||||
// Element-specific scrolling if element is provided
|
// Element-specific scrolling if element is provided
|
||||||
if (element) {
|
if (element) {
|
||||||
const targetElement = element
|
const targetElement = element
|
||||||
@@ -421,11 +425,14 @@ export async function scrollHorizontally(
|
|||||||
let scrolledElement: HTMLElement | null = null
|
let scrolledElement: HTMLElement | null = null
|
||||||
let scrollDelta = 0
|
let scrollDelta = 0
|
||||||
let attempts = 0
|
let attempts = 0
|
||||||
const dx = right ? scroll_amount : -scroll_amount
|
const dx = scroll_amount
|
||||||
|
|
||||||
while (currentElement && attempts < 10) {
|
while (currentElement && attempts < 10) {
|
||||||
const computedStyle = window.getComputedStyle(currentElement)
|
const computedStyle = window.getComputedStyle(currentElement)
|
||||||
const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX)
|
const hasScrollableX =
|
||||||
|
/(auto|scroll|overlay)/.test(computedStyle.overflowX) ||
|
||||||
|
(computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
|
||||||
|
(computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
|
||||||
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
|
||||||
|
|
||||||
if (hasScrollableX && canScrollHorizontally) {
|
if (hasScrollableX && canScrollHorizontally) {
|
||||||
@@ -469,7 +476,8 @@ export async function scrollHorizontally(
|
|||||||
|
|
||||||
// Page-level scrolling (default or fallback)
|
// Page-level scrolling (default or fallback)
|
||||||
|
|
||||||
const dx = right ? scroll_amount : -scroll_amount
|
const dx = scroll_amount
|
||||||
|
|
||||||
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
|
const bigEnough = (el: HTMLElement) => el.clientWidth >= window.innerWidth * 0.5
|
||||||
const canScroll = (el: HTMLElement | null) =>
|
const canScroll = (el: HTMLElement | null) =>
|
||||||
el &&
|
el &&
|
||||||
@@ -477,6 +485,9 @@ export async function scrollHorizontally(
|
|||||||
el.scrollWidth > el.clientWidth &&
|
el.scrollWidth > el.clientWidth &&
|
||||||
bigEnough(el)
|
bigEnough(el)
|
||||||
|
|
||||||
|
// @deprecated Same heuristic container search as scrollVertically.
|
||||||
|
// TODO: Remove once LLMs reliably use indexed scrolling via data-scrollable.
|
||||||
|
|
||||||
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
let el: HTMLElement | null = document.activeElement as HTMLElement | null
|
||||||
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
while (el && !canScroll(el) && el !== document.body) el = el.parentElement
|
||||||
|
|
||||||
@@ -511,6 +522,9 @@ export async function scrollHorizontally(
|
|||||||
return `✅ Scrolled page horizontally by ${scrolled}px.`
|
return `✅ Scrolled page horizontally by ${scrolled}px.`
|
||||||
} else {
|
} else {
|
||||||
// Container scroll
|
// Container scroll
|
||||||
|
const warningMsg = `The document is not scrollable. Falling back to container scroll.`
|
||||||
|
console.log(`[PageController] ${warningMsg}`)
|
||||||
|
|
||||||
const scrollBefore = el!.scrollLeft
|
const scrollBefore = el!.scrollLeft
|
||||||
const scrollMax = el!.scrollWidth - el!.clientWidth
|
const scrollMax = el!.scrollWidth - el!.clientWidth
|
||||||
|
|
||||||
@@ -522,17 +536,17 @@ export async function scrollHorizontally(
|
|||||||
|
|
||||||
if (Math.abs(scrolled) < 1) {
|
if (Math.abs(scrolled) < 1) {
|
||||||
return dx > 0
|
return dx > 0
|
||||||
? `⚠️ Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
|
? `⚠️ ${warningMsg} Already at the right edge of container (${el!.tagName}), cannot scroll right further.`
|
||||||
: `⚠️ Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
|
: `⚠️ ${warningMsg} Already at the left edge of container (${el!.tagName}), cannot scroll left further.`
|
||||||
}
|
}
|
||||||
|
|
||||||
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
|
||||||
const reachedLeft = dx < 0 && scrollAfter <= 1
|
const reachedLeft = dx < 0 && scrollAfter <= 1
|
||||||
|
|
||||||
if (reachedRight)
|
if (reachedRight)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the right edge.`
|
||||||
if (reachedLeft)
|
if (reachedLeft)
|
||||||
return `✅ Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) by ${scrolled}px. Reached the left edge.`
|
||||||
return `✅ Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
|
return `✅ ${warningMsg} Scrolled container (${el!.tagName}) horizontally by ${scrolled}px.`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { FlatDomTree } from './type'
|
||||||
|
|
||||||
|
interface DomTreeArgs {
|
||||||
|
doHighlightElements?: boolean
|
||||||
|
focusHighlightIndex?: number
|
||||||
|
viewportExpansion?: number
|
||||||
|
debugMode?: boolean
|
||||||
|
interactiveBlacklist?: Element[]
|
||||||
|
interactiveWhitelist?: Element[]
|
||||||
|
highlightOpacity?: number
|
||||||
|
highlightLabelOpacity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const domTree: (args?: DomTreeArgs) => FlatDomTree
|
||||||
|
|
||||||
|
export default domTree
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
* @edit improve `sampleRect`, filter out rects with 0 area
|
* @edit improve `sampleRect`, filter out rects with 0 area
|
||||||
* @edit exclude aria-hidden elements
|
* @edit exclude aria-hidden elements
|
||||||
* @edit make sure attributes exist for interactive candidates.
|
* @edit make sure attributes exist for interactive candidates.
|
||||||
|
* @edit fix "aria-*" attributes check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
@@ -502,11 +503,16 @@ export default (
|
|||||||
const overflowX = style.overflowX
|
const overflowX = style.overflowX
|
||||||
const overflowY = style.overflowY
|
const overflowY = style.overflowY
|
||||||
|
|
||||||
// Check scrollable distances
|
// scrollbar-width/scrollbar-gutter are only set on elements designed to scroll;
|
||||||
|
// their presence signals scroll intent even when overflow is hidden (e.g. overflow: auto on :hover)
|
||||||
|
const hasScrollbarSignal =
|
||||||
|
(style.scrollbarWidth && style.scrollbarWidth !== 'auto') ||
|
||||||
|
(style.scrollbarGutter && style.scrollbarGutter !== 'auto')
|
||||||
|
|
||||||
const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
|
const scrollableX = overflowX === 'auto' || overflowX === 'scroll'
|
||||||
const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
|
const scrollableY = overflowY === 'auto' || overflowY === 'scroll'
|
||||||
|
|
||||||
if (!scrollableX && !scrollableY) {
|
if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
|
||||||
return null // Not scrollable in any direction
|
return null // Not scrollable in any direction
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,11 +526,11 @@ export default (
|
|||||||
return null // Not scrollable
|
return null // Not scrollable
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollableY && scrollWidth < threshold) {
|
if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
|
||||||
return null // Not scrollable horizontally
|
return null // Not scrollable horizontally
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!scrollableX && scrollHeight < threshold) {
|
if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
|
||||||
return null // Not scrollable vertically
|
return null // Not scrollable vertically
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -546,6 +552,8 @@ export default (
|
|||||||
scrollData: scrollData,
|
scrollData: scrollData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log('scrollData!!!', scrollData)
|
||||||
|
|
||||||
return scrollData
|
return scrollData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,6 +1151,31 @@ export default (
|
|||||||
* @param {HTMLElement} element - The element to check.
|
* @param {HTMLElement} element - The element to check.
|
||||||
* @returns {boolean} Whether the element is an interactive candidate.
|
* @returns {boolean} Whether the element is an interactive candidate.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// @edit fix "aria-*" attributes check
|
||||||
|
const INTERACTIVE_ARIA_ATTRS = [
|
||||||
|
'aria-expanded',
|
||||||
|
'aria-checked',
|
||||||
|
'aria-selected',
|
||||||
|
'aria-pressed',
|
||||||
|
'aria-haspopup',
|
||||||
|
'aria-controls',
|
||||||
|
'aria-owns',
|
||||||
|
'aria-activedescendant',
|
||||||
|
'aria-valuenow',
|
||||||
|
'aria-valuetext',
|
||||||
|
'aria-valuemax',
|
||||||
|
'aria-valuemin',
|
||||||
|
'aria-autocomplete',
|
||||||
|
]
|
||||||
|
|
||||||
|
function hasInteractiveAria(el) {
|
||||||
|
for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
|
||||||
|
if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function isInteractiveCandidate(element) {
|
function isInteractiveCandidate(element) {
|
||||||
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
|
if (!element || element.nodeType !== Node.ELEMENT_NODE) return false
|
||||||
|
|
||||||
@@ -1167,7 +1200,7 @@ export default (
|
|||||||
element.hasAttribute('onclick') ||
|
element.hasAttribute('onclick') ||
|
||||||
element.hasAttribute('role') ||
|
element.hasAttribute('role') ||
|
||||||
element.hasAttribute('tabindex') ||
|
element.hasAttribute('tabindex') ||
|
||||||
element.hasAttribute('aria-') ||
|
hasInteractiveAria(element) ||
|
||||||
element.hasAttribute('data-action') ||
|
element.hasAttribute('data-action') ||
|
||||||
element.getAttribute('contenteditable') === 'true'
|
element.getAttribute('contenteditable') === 'true'
|
||||||
|
|
||||||
@@ -1185,8 +1218,9 @@ export default (
|
|||||||
'details',
|
'details',
|
||||||
'label',
|
'label',
|
||||||
'option',
|
'option',
|
||||||
|
'li',
|
||||||
])
|
])
|
||||||
const INTERACTIVE_ROLES = new Set([
|
const DISTINCT_INTERACTIVE_ROLES = new Set([
|
||||||
'button',
|
'button',
|
||||||
'link',
|
'link',
|
||||||
'menuitem',
|
'menuitem',
|
||||||
@@ -1202,6 +1236,9 @@ export default (
|
|||||||
'searchbox',
|
'searchbox',
|
||||||
'textbox',
|
'textbox',
|
||||||
'listbox',
|
'listbox',
|
||||||
|
'listitem',
|
||||||
|
'treeitem',
|
||||||
|
'row',
|
||||||
'option',
|
'option',
|
||||||
'scrollbar',
|
'scrollbar',
|
||||||
])
|
])
|
||||||
@@ -1278,7 +1315,7 @@ export default (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Check interactive roles
|
// Check interactive roles
|
||||||
if (role && INTERACTIVE_ROLES.has(role)) {
|
if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// Check contenteditable
|
// Check contenteditable
|
||||||
@@ -1297,6 +1334,10 @@ export default (
|
|||||||
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
|
if (element.hasAttribute('onclick') || typeof element.onclick === 'function') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
// ARIA state attributes imply the element manages its own interaction state
|
||||||
|
if (hasInteractiveAria(element)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// return false
|
// return false
|
||||||
|
|
||||||
@@ -1352,6 +1393,12 @@ export default (
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrollable containers are always distinct — the LLM needs their index for targeted scrolling.
|
||||||
|
// Check extraData (already set by isScrollableElement in isInteractiveElement) to avoid redundant layout reads.
|
||||||
|
if (extraData.get(element)?.scrollable) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Default to false: if it's interactive but doesn't match above,
|
// Default to false: if it's interactive but doesn't match above,
|
||||||
// assume it triggers the same action as the parent.
|
// assume it triggers the same action as the parent.
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -28,8 +28,27 @@ export interface DomConfig {
|
|||||||
includeAttributes?: string[]
|
includeAttributes?: string[]
|
||||||
highlightOpacity?: number
|
highlightOpacity?: number
|
||||||
highlightLabelOpacity?: number
|
highlightLabelOpacity?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preserve semantic landmark tags in dehydrated output even if not interactive
|
||||||
|
* @note maybe confusing for LLM combining with page scrolling, use with caution
|
||||||
|
**/
|
||||||
|
keepSemanticTags?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: corresponding roles
|
||||||
|
const SEMANTIC_TAGS = new Set([
|
||||||
|
'nav',
|
||||||
|
'menu',
|
||||||
|
// 'main',
|
||||||
|
'header',
|
||||||
|
'footer',
|
||||||
|
'aside',
|
||||||
|
// 'article',
|
||||||
|
// 'form',
|
||||||
|
'dialog',
|
||||||
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用于检测可交互元素是否是新出现的。
|
* 用于检测可交互元素是否是新出现的。
|
||||||
*/
|
*/
|
||||||
@@ -171,7 +190,11 @@ interface TreeNode {
|
|||||||
*
|
*
|
||||||
* @todo 数据脱敏过滤器
|
* @todo 数据脱敏过滤器
|
||||||
*/
|
*/
|
||||||
export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: string[]): string {
|
export function flatTreeToString(
|
||||||
|
flatTree: FlatDomTree,
|
||||||
|
includeAttributes: string[] = [],
|
||||||
|
keepSemanticTags = false
|
||||||
|
): string {
|
||||||
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
const DEFAULT_INCLUDE_ATTRIBUTES = [
|
||||||
'title',
|
'title',
|
||||||
'type',
|
'type',
|
||||||
@@ -203,7 +226,7 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
'contenteditable',
|
'contenteditable',
|
||||||
]
|
]
|
||||||
|
|
||||||
const includeAttrs = [...(includeAttributes || []), ...DEFAULT_INCLUDE_ATTRIBUTES]
|
const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES]
|
||||||
|
|
||||||
// Helper function to cap text length
|
// Helper function to cap text length
|
||||||
const capTextLength = (text: string, maxLength: number): string => {
|
const capTextLength = (text: string, maxLength: number): string => {
|
||||||
@@ -294,6 +317,8 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
const depthStr = '\t'.repeat(depth)
|
const depthStr = '\t'.repeat(depth)
|
||||||
|
|
||||||
if (node.type === 'element') {
|
if (node.type === 'element') {
|
||||||
|
const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName)
|
||||||
|
|
||||||
// Add element with highlight_index
|
// Add element with highlight_index
|
||||||
if (node.highlightIndex !== undefined) {
|
if (node.highlightIndex !== undefined) {
|
||||||
nextDepth += 1
|
nextDepth += 1
|
||||||
@@ -391,10 +416,30 @@ export function flatTreeToString(flatTree: FlatDomTree, includeAttributes?: stri
|
|||||||
result.push(line)
|
result.push(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process children regardless
|
// special treatment for semantic tags
|
||||||
|
// even if they are not interactive, we can keep them for clear context
|
||||||
|
|
||||||
|
const emitSemantic = isSemantic && node.highlightIndex === undefined
|
||||||
|
// to check if this tag is empty
|
||||||
|
const mark = emitSemantic ? result.length : -1
|
||||||
|
|
||||||
|
if (emitSemantic) {
|
||||||
|
result.push(`${depthStr}<${node.tagName}>`)
|
||||||
|
nextDepth += 1
|
||||||
|
}
|
||||||
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
processNode(child, nextDepth, result)
|
processNode(child, nextDepth, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emitSemantic) {
|
||||||
|
// empty tag should be removed
|
||||||
|
if (result.length === mark + 1) {
|
||||||
|
result.pop()
|
||||||
|
} else {
|
||||||
|
result.push(`${depthStr}</${node.tagName}>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (node.type === 'text') {
|
} else if (node.type === 'text') {
|
||||||
// Add text only if it doesn't have a highlighted parent
|
// Add text only if it doesn't have a highlighted parent
|
||||||
if (hasParentWithHighlightIndex(node)) {
|
if (hasParentWithHighlightIndex(node)) {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export class SimulatorMask extends EventTarget {
|
|||||||
wrapper = document.createElement('div')
|
wrapper = document.createElement('div')
|
||||||
motion: Motion | null = null
|
motion: Motion | null = null
|
||||||
|
|
||||||
|
#disposed = false
|
||||||
|
|
||||||
#cursor = document.createElement('div')
|
#cursor = document.createElement('div')
|
||||||
|
|
||||||
#currentCursorX = 0
|
#currentCursorX = 0
|
||||||
@@ -129,6 +131,8 @@ export class SimulatorMask extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#moveCursorToTarget() {
|
#moveCursorToTarget() {
|
||||||
|
if (this.#disposed) return
|
||||||
|
|
||||||
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
|
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
|
||||||
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2
|
const newY = this.#currentCursorY + (this.#targetCursorY - this.#currentCursorY) * 0.2
|
||||||
|
|
||||||
@@ -156,11 +160,15 @@ export class SimulatorMask extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCursorPosition(x: number, y: number) {
|
setCursorPosition(x: number, y: number) {
|
||||||
|
if (this.#disposed) return
|
||||||
|
|
||||||
this.#targetCursorX = x
|
this.#targetCursorX = x
|
||||||
this.#targetCursorY = y
|
this.#targetCursorY = y
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerClickAnimation() {
|
triggerClickAnimation() {
|
||||||
|
if (this.#disposed) return
|
||||||
|
|
||||||
this.#cursor.classList.remove(cursorStyles.clicking)
|
this.#cursor.classList.remove(cursorStyles.clicking)
|
||||||
// Force reflow to restart animation
|
// Force reflow to restart animation
|
||||||
void this.#cursor.offsetHeight
|
void this.#cursor.offsetHeight
|
||||||
@@ -168,7 +176,7 @@ export class SimulatorMask extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
show() {
|
show() {
|
||||||
if (this.shown) return
|
if (this.shown || this.#disposed) return
|
||||||
|
|
||||||
this.shown = true
|
this.shown = true
|
||||||
this.motion?.start()
|
this.motion?.start()
|
||||||
@@ -186,7 +194,7 @@ export class SimulatorMask extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
if (!this.shown) return
|
if (!this.shown || this.#disposed) return
|
||||||
|
|
||||||
this.shown = false
|
this.shown = false
|
||||||
this.motion?.fadeOut()
|
this.motion?.fadeOut()
|
||||||
@@ -200,6 +208,7 @@ export class SimulatorMask extends EventTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispose() {
|
dispose() {
|
||||||
|
this.#disposed = true
|
||||||
console.log('dispose SimulatorMask')
|
console.log('dispose SimulatorMask')
|
||||||
this.motion?.dispose()
|
this.motion?.dispose()
|
||||||
this.wrapper.remove()
|
this.wrapper.remove()
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
// @workaround DTS bug
|
|
||||||
// dts do not work with monorepo path mapping
|
|
||||||
// disable path mapping for it
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.js"],
|
"include": ["**/*.ts", "**/*.js"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
|
|||||||
@@ -13,13 +13,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/page-controller`))
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
dts({
|
||||||
|
bundleTypes: true,
|
||||||
|
compilerOptions: {
|
||||||
|
composite: true,
|
||||||
|
noEmit: false,
|
||||||
|
emitDeclarationOnly: true,
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
],
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/PageController.ts'),
|
entry: resolve(__dirname, 'src/PageController.ts'),
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/ui",
|
"name": "@page-agent/ui",
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/lib/page-agent-ui.js",
|
"main": "./src/index.ts",
|
||||||
"module": "./dist/lib/page-agent-ui.js",
|
"types": "./src/index.ts",
|
||||||
"types": "./dist/lib/index.d.ts",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/lib/index.d.ts",
|
"types": "./src/index.ts",
|
||||||
"import": "./dist/lib/page-agent-ui.js",
|
"default": "./src/index.ts"
|
||||||
"default": "./dist/lib/page-agent-ui.js"
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"main": "./dist/lib/page-agent-ui.js",
|
||||||
|
"module": "./dist/lib/page-agent-ui.js",
|
||||||
|
"types": "./dist/lib/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/lib/index.d.ts",
|
||||||
|
"import": "./dist/lib/page-agent-ui.js",
|
||||||
|
"default": "./dist/lib/page-agent-ui.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -32,7 +42,7 @@
|
|||||||
"homepage": "https://alibaba.github.io/page-agent/",
|
"homepage": "https://alibaba.github.io/page-agent/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"prepublishOnly": "node -e \"const fs=require('fs');['LICENSE'].forEach(f=>fs.copyFileSync('../../'+f,f))\"",
|
"prepublishOnly": "node ../../scripts/pre-publish.js",
|
||||||
"postpublish": "node -e \"['LICENSE'].forEach(f=>{try{require('fs').unlinkSync(f)}catch{}})\""
|
"postpublish": "node ../../scripts/post-publish.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,7 +291,7 @@
|
|||||||
transition: max-height 0.2s;
|
transition: max-height 0.2s;
|
||||||
|
|
||||||
.expanded & {
|
.expanded & {
|
||||||
max-height: 400px;
|
max-height: min(500px, calc(100vh - 200px - var(--height)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.historyItem {
|
.historyItem {
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
// @workaround DTS bug
|
|
||||||
// dts do not work with monorepo path mapping
|
|
||||||
// disable path mapping for it
|
|
||||||
"paths": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
|
||||||
"noEmit": false,
|
|
||||||
"allowImportingTsExtensions": false,
|
|
||||||
"baseUrl": ".",
|
|
||||||
"outDir": "dist"
|
|
||||||
},
|
},
|
||||||
"include": ["**/*.ts", "**/*.js"],
|
"include": ["**/*.ts", "**/*.js"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
|
|||||||
@@ -13,13 +13,18 @@ console.log(chalk.cyan(`📦 Building @page-agent/ui`))
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
clearScreen: false,
|
clearScreen: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({ tsconfigPath: './tsconfig.dts.json', bundleTypes: true }),
|
dts({
|
||||||
|
bundleTypes: true,
|
||||||
|
compilerOptions: {
|
||||||
|
composite: true,
|
||||||
|
noEmit: false,
|
||||||
|
emitDeclarationOnly: true,
|
||||||
|
declaration: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
cssInjectedByJsPlugin({ relativeCSSInjection: true }),
|
||||||
],
|
],
|
||||||
publicDir: false,
|
publicDir: false,
|
||||||
esbuild: {
|
|
||||||
keepNames: true,
|
|
||||||
},
|
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'src/index.ts'),
|
entry: resolve(__dirname, 'src/index.ts'),
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ Uses wouter browser routing with base path for GitHub Pages deployment at `https
|
|||||||
```tsx
|
```tsx
|
||||||
// main.tsx
|
// main.tsx
|
||||||
<Router base="/page-agent">
|
<Router base="/page-agent">
|
||||||
<PagesRouter />
|
<PagesRouter />
|
||||||
</Router>
|
</Router>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -109,6 +109,7 @@ Instead of `404.html` redirects, the build copies `index.html` into every route
|
|||||||
2. Add route in `src/pages/docs/index.tsx`
|
2. Add route in `src/pages/docs/index.tsx`
|
||||||
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
3. Add navigation item in `src/pages/docs/Layout.tsx`
|
||||||
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
4. Add path to `SPA_ROUTES` in `vite.config.js`
|
||||||
|
5. Keep `<slug>` consistent across the folder name, import path, route path, sidebar link, and any related cross-links. If the public route changes, rename the folder too.
|
||||||
|
|
||||||
## Configuration Files
|
## Configuration Files
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,34 @@
|
|||||||
<div id="sk">
|
<div id="sk">
|
||||||
<p class="sk-text" id="sk-text">Loading...</p>
|
<p class="sk-text" id="sk-text">Loading...</p>
|
||||||
<style>
|
<style>
|
||||||
#sk{display:flex;align-items:center;justify-content:center;min-height:100vh;background:linear-gradient(135deg,#eff6ff,#f5f3ff)}
|
#sk {
|
||||||
@media(prefers-color-scheme:dark){#sk{background:linear-gradient(135deg,#111827,#1f2937)}}
|
display: flex;
|
||||||
.sk-text{font:400 14px/1 system-ui,sans-serif;color:#94a3b8;animation:skf 2s ease-in-out infinite}
|
align-items: center;
|
||||||
@keyframes skf{0%,100%{opacity:.6}50%{opacity:.3}}
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #eff6ff, #f5f3ff);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#sk {
|
||||||
|
background: linear-gradient(135deg, #111827, #1f2937);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sk-text {
|
||||||
|
font:
|
||||||
|
400 14px/1 system-ui,
|
||||||
|
sans-serif;
|
||||||
|
color: #94a3b8;
|
||||||
|
animation: skf 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes skf {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@page-agent/website",
|
"name": "@page-agent/website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.7.0",
|
"version": "1.8.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0",
|
"dev": "vite --host 0.0.0.0",
|
||||||
"build:website": "vite build",
|
"build:website": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview"
|
||||||
"typecheck": "tsc --noEmit"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
@@ -16,19 +15,19 @@
|
|||||||
"@radix-ui/react-switch": "^1.2.6",
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.1",
|
"@types/react-dom": "^19.2.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.14.0",
|
||||||
"motion": "^12.38.0",
|
"motion": "^12.38.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.6",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.6",
|
||||||
"rough-notation": "^0.5.1",
|
"rough-notation": "^0.5.1",
|
||||||
"simple-icons": "^16.14.0",
|
"simple-icons": "^16.19.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.5.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.3.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"wouter": "^3.9.0"
|
"wouter": "^3.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function Header() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header
|
<header
|
||||||
className="relative z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
|
className="sticky top-0 z-50 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md border-b border-gray-200 dark:border-gray-700"
|
||||||
role="banner"
|
role="banner"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto px-6 py-4">
|
<div className="max-w-7xl mx-auto px-6 py-4">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ComponentPropsWithoutRef, useEffect, useRef } from 'react'
|
|||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
type Level = 2 | 3
|
type Level = 2 | 3 | 4
|
||||||
|
|
||||||
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'> {
|
||||||
id: string
|
id: string
|
||||||
@@ -11,8 +11,9 @@ interface HeadingProps extends Omit<ComponentPropsWithoutRef<'h2'>, 'children'>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const levelStyles = {
|
const levelStyles = {
|
||||||
2: { tag: 'h2', className: 'text-2xl font-semibold mb-4' },
|
2: { tag: 'h2', className: 'text-3xl font-semibold mb-4' },
|
||||||
3: { tag: 'h3', className: 'text-xl font-semibold mb-3' },
|
3: { tag: 'h3', className: 'text-[1.375rem] font-semibold mb-3' },
|
||||||
|
4: { tag: 'h4', className: 'text-[1.0625rem] font-medium mb-2' },
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
export function Heading({ id, level = 2, className, children, ...props }: HeadingProps) {
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ function highlightSyntax(code: string): string {
|
|||||||
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
|
const HighlightSyntaxClient: React.FC<HighlightSyntaxProps> = ({ code }) => {
|
||||||
const htmlContent = highlightSyntax(code)
|
const htmlContent = highlightSyntax(code)
|
||||||
|
|
||||||
// eslint-disable-next-line react-dom/no-dangerously-set-innerhtml
|
|
||||||
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
return <code className={styles.syntax} dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ function JSConsole({
|
|||||||
// 全局console拦截处理
|
// 全局console拦截处理
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interceptor = ConsoleInterceptor.getInstance()
|
const interceptor = ConsoleInterceptor.getInstance()
|
||||||
|
let scrollTimer: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
const handleGlobalConsole = (type: string, args: unknown[]) => {
|
||||||
const content = args.map((arg) => formatResult(arg)).join(' ')
|
const content = args.map((arg) => formatResult(arg)).join(' ')
|
||||||
@@ -152,8 +153,8 @@ function JSConsole({
|
|||||||
|
|
||||||
setOutputs((prev) => [...prev, outputItem])
|
setOutputs((prev) => [...prev, outputItem])
|
||||||
|
|
||||||
// 自动滚动到底部
|
clearTimeout(scrollTimer)
|
||||||
setTimeout(() => {
|
scrollTimer = setTimeout(() => {
|
||||||
if (outputRef.current) {
|
if (outputRef.current) {
|
||||||
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
outputRef.current.scrollTop = outputRef.current.scrollHeight
|
||||||
}
|
}
|
||||||
@@ -164,6 +165,7 @@ function JSConsole({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
interceptor.unsubscribe(handleGlobalConsole)
|
interceptor.unsubscribe(handleGlobalConsole)
|
||||||
|
clearTimeout(scrollTimer)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Demo build (auto-init with demo LLM, for quick testing)
|
// Demo build (auto-init with demo LLM, for quick testing)
|
||||||
export const CDN_DEMO_URL =
|
export const CDN_DEMO_URL =
|
||||||
'https://cdn.jsdelivr.net/npm/page-agent@1.7.0/dist/iife/page-agent.demo.js'
|
'https://cdn.jsdelivr.net/npm/page-agent@1.8.2/dist/iife/page-agent.demo.js'
|
||||||
export const CDN_DEMO_CN_URL =
|
export const CDN_DEMO_CN_URL =
|
||||||
'https://registry.npmmirror.com/page-agent/1.7.0/files/dist/iife/page-agent.demo.js'
|
'https://registry.npmmirror.com/page-agent/1.8.2/files/dist/iife/page-agent.demo.js'
|
||||||
|
|
||||||
// Demo LLM for website testing (homepage quick trial uses flash)
|
// Demo LLM for website testing (homepage quick trial uses flash)
|
||||||
export const DEMO_MODEL = 'qwen3.5-flash'
|
export const DEMO_MODEL = 'qwen3.5-flash'
|
||||||
|
|||||||
@@ -9,25 +9,24 @@ const LanguageContext = createContext<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||||
const [language, setLang] = useState<Lang>(() => {
|
const [language, setLanguage] = useState<Lang>(() => {
|
||||||
const stored = localStorage.getItem('language') as Lang
|
const stored = localStorage.getItem('language') as Lang
|
||||||
if (stored === 'zh-CN' || stored === 'en-US') return stored
|
if (stored === 'zh-CN' || stored === 'en-US') return stored
|
||||||
return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'
|
return navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||||
})
|
})
|
||||||
|
|
||||||
const setLanguage = (lang: Lang) => {
|
const switchLanguage = (lang: Lang) => {
|
||||||
setLang(lang)
|
setLanguage(lang)
|
||||||
localStorage.setItem('language', lang)
|
localStorage.setItem('language', lang)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage }}>
|
<LanguageContext value={{ language, isZh: language === 'zh-CN', setLanguage: switchLanguage }}>
|
||||||
{children}
|
{children}
|
||||||
</LanguageContext>
|
</LanguageContext>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-refresh/only-export-components
|
|
||||||
export function useLanguage() {
|
export function useLanguage() {
|
||||||
const ctx = use(LanguageContext)
|
const ctx = use(LanguageContext)
|
||||||
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider')
|
if (!ctx) throw new Error('useLanguage must be used within LanguageProvider')
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ body {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题使用中等字重(相对细体更重,但比默认 bold 更轻) */
|
/* 文档标题层级需要比正文标题更清晰,避免 h2/h3/h4 视觉差异过小。 */
|
||||||
.prose h1,
|
.prose h1,
|
||||||
.prose h2,
|
.prose h2,
|
||||||
.prose h3,
|
.prose h3,
|
||||||
@@ -103,6 +103,33 @@ body {
|
|||||||
.prose h5,
|
.prose h5,
|
||||||
.prose h6 {
|
.prose h6 {
|
||||||
font-weight: 480;
|
font-weight: 480;
|
||||||
|
line-height: 1.25;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h2[id] {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 0.45rem;
|
||||||
|
border-bottom: 1px solid rgba(23, 23, 23, 0.12);
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h3[id] {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 1.375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose h4[id] {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
font-weight: 560;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .prose h2[id] {
|
||||||
|
border-bottom-color: rgba(255, 255, 255, 0.14);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* strong/b 也用中等字重 */
|
/* strong/b 也用中等字重 */
|
||||||
|
|||||||
@@ -156,6 +156,34 @@ const result = await agent.execute('Fill in the form with test data')`}
|
|||||||
defaultValue: '3',
|
defaultValue: '3',
|
||||||
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
description: isZh ? 'API 调用失败时的最大重试次数' : 'Maximum retries on API failure',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'transformRequestBody',
|
||||||
|
type: '(requestBody) => Record<string, unknown> | undefined',
|
||||||
|
description: isZh ? (
|
||||||
|
<>
|
||||||
|
在请求发送前转换最终 request body。可用于处理供应商特定的缓存提示或私有参数。查看{' '}
|
||||||
|
<Link
|
||||||
|
href="/features/models#prompt-caching"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
主动缓存示例
|
||||||
|
</Link>
|
||||||
|
。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Transform the final request body before sending it. Useful for provider-specific
|
||||||
|
cache hints or private request parameters. See{' '}
|
||||||
|
<Link
|
||||||
|
href="/features/models#prompt-caching"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
prompt caching examples
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'disableNamedToolChoice',
|
name: 'disableNamedToolChoice',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
|
|||||||
@@ -108,6 +108,14 @@ const agent = new PageAgentCore({
|
|||||||
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
|
? '在 DOM 提取中包含的额外 HTML 属性。支持通配符 *(如 data-* 匹配所有 data- 开头的属性)。默认已包含常见属性如 role, aria-label 等。'
|
||||||
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
|
: 'Additional HTML attributes to include in DOM extraction. Supports wildcard * (e.g. data-* matches all data- prefixed attributes). Common attributes like role, aria-label are included by default.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'keepSemanticTags',
|
||||||
|
type: 'boolean',
|
||||||
|
defaultValue: 'false',
|
||||||
|
description: isZh
|
||||||
|
? '在简化输出中保留语义标签(如 nav, main, header, footer, aside 等),即使它们不可交互。帮助 LLM 理解页面结构。'
|
||||||
|
: 'Preserve semantic landmark tags (e.g. nav, main, header, footer, aside) in dehydrated output even if not interactive. Helps LLM understand page structure.',
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ interface ExecuteConfig {
|
|||||||
model: string // Model name
|
model: string // Model name
|
||||||
apiKey?: string // LLM AK
|
apiKey?: string // LLM AK
|
||||||
|
|
||||||
|
systemInstruction?: string // Global system-level instructions
|
||||||
includeInitialTab?: boolean
|
includeInitialTab?: boolean
|
||||||
experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window
|
experimentalIncludeAllTabs?: boolean // Control all unpinned tabs in the window
|
||||||
onStatusChange?: (status: AgentStatus) => void
|
onStatusChange?: (status: AgentStatus) => void
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
|
import { Link } from 'wouter'
|
||||||
|
|
||||||
import CodeEditor from '@/components/CodeEditor'
|
import CodeEditor from '@/components/CodeEditor'
|
||||||
import { Heading } from '@/components/Heading'
|
import { Heading } from '@/components/Heading'
|
||||||
@@ -6,9 +7,10 @@ import { useLanguage } from '@/i18n/context'
|
|||||||
|
|
||||||
const BASELINE = new Set([
|
const BASELINE = new Set([
|
||||||
'gpt-5.1',
|
'gpt-5.1',
|
||||||
|
'gpt-5.4-mini',
|
||||||
'claude-haiku-4.5',
|
'claude-haiku-4.5',
|
||||||
'gemini-3-flash',
|
'gemini-3.1-flash-lite',
|
||||||
'deepseek-3.2',
|
'deepseek-v4-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
])
|
])
|
||||||
@@ -16,17 +18,31 @@ const BASELINE = new Set([
|
|||||||
// Models grouped by brand, newest first
|
// Models grouped by brand, newest first
|
||||||
const MODEL_GROUPS: Record<string, string[]> = {
|
const MODEL_GROUPS: Record<string, string[]> = {
|
||||||
Qwen: [
|
Qwen: [
|
||||||
|
'qwen3.6-max',
|
||||||
|
'qwen3.6-plus',
|
||||||
|
'qwen3.6-flash',
|
||||||
'qwen3.5-plus',
|
'qwen3.5-plus',
|
||||||
'qwen3.5-flash',
|
'qwen3.5-flash',
|
||||||
'qwen3-coder-next',
|
'qwen3-coder-next',
|
||||||
'qwen-3-max',
|
'qwen-3-max',
|
||||||
'qwen-3-plus',
|
'qwen-3-plus',
|
||||||
'qwen3:14b (ollama)',
|
|
||||||
],
|
],
|
||||||
OpenAI: ['gpt-5.4', 'gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-4.1', 'gpt-4.1-mini'],
|
OpenAI: [
|
||||||
DeepSeek: ['deepseek-3.2'],
|
'gpt-5.5',
|
||||||
Google: ['gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
|
'gpt-5.4',
|
||||||
|
'gpt-5.4-mini',
|
||||||
|
'gpt-5.4-nano',
|
||||||
|
'gpt-5.2',
|
||||||
|
'gpt-5.1',
|
||||||
|
'gpt-5',
|
||||||
|
'gpt-5-mini',
|
||||||
|
'gpt-4.1',
|
||||||
|
'gpt-4.1-mini',
|
||||||
|
],
|
||||||
|
DeepSeek: ['deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-3.2'],
|
||||||
|
Google: ['gemini-3.1-flash-lite', 'gemini-3-pro', 'gemini-3-flash', 'gemini-2.5'],
|
||||||
Anthropic: [
|
Anthropic: [
|
||||||
|
'claude-opus-4.7',
|
||||||
'claude-opus-4.6',
|
'claude-opus-4.6',
|
||||||
'claude-opus-4.5',
|
'claude-opus-4.5',
|
||||||
'claude-sonnet-4.5',
|
'claude-sonnet-4.5',
|
||||||
@@ -64,7 +80,6 @@ export default function Models() {
|
|||||||
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
|
: 'Supports models that comply with OpenAI API specification and support tool calls, including public cloud services and private deployments.'}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Models Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
<Heading id="tested-models" className="text-2xl font-semibold mb-3">
|
||||||
{isZh ? '已测试模型' : 'Tested Models'}
|
{isZh ? '已测试模型' : 'Tested Models'}
|
||||||
@@ -87,9 +102,10 @@ export default function Models() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Tips Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<h2 className="text-2xl font-semibold mb-4">{isZh ? '提示' : 'Tips'}</h2>
|
<Heading id="tips" level={3}>
|
||||||
|
Tips
|
||||||
|
</Heading>
|
||||||
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
<div className="p-4 bg-blue-50 dark:bg-blue-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
<li>
|
<li>
|
||||||
@@ -111,7 +127,6 @@ export default function Models() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Configuration Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
|
<Heading id="configuration">{isZh ? '配置方式' : 'Configuration'}</Heading>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
@@ -121,25 +136,10 @@ const pageAgent = new PageAgent({
|
|||||||
apiKey: 'your-api-key',
|
apiKey: 'your-api-key',
|
||||||
model: 'qwen3.5-plus'
|
model: 'qwen3.5-plus'
|
||||||
});
|
});
|
||||||
|
|
||||||
// MiniMax
|
|
||||||
const pageAgent = new PageAgent({
|
|
||||||
baseURL: 'https://api.minimax.io/v1',
|
|
||||||
apiKey: 'your-minimax-api-key',
|
|
||||||
model: 'MiniMax-M2.7'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Self-hosted models (e.g., Ollama) — no apiKey needed
|
|
||||||
const pageAgent = new PageAgent({
|
|
||||||
baseURL: 'http://localhost:11434/v1',
|
|
||||||
model: 'qwen3:14b'
|
|
||||||
});
|
|
||||||
|
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Free Testing API Section */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
<Heading id="free-testing-api">{isZh ? '免费测试接口' : 'Free Testing API'}</Heading>
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
@@ -181,78 +181,13 @@ const pageAgent = new PageAgent({
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
code={`# qwen3.5-plus (default for demos) or qwen3.5-flash (lighter)
|
code={`# qwen3.5-plus / qwen3.5-flash
|
||||||
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
|
LLM_BASE_URL="https://page-ag-testing-ohftxirgbn.cn-shanghai.fcapp.run"
|
||||||
LLM_MODEL_NAME="qwen3.5-plus"
|
LLM_MODEL_NAME="qwen3.5-plus"`}
|
||||||
LLM_API_KEY="NA"`}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Ollama Section */}
|
|
||||||
<section className="mb-10">
|
|
||||||
<Heading id="ollama">Ollama</Heading>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{isZh
|
|
||||||
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
|
||||||
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
code={`LLM_BASE_URL="http://localhost:11434/v1"
|
|
||||||
LLM_API_KEY="NA"
|
|
||||||
LLM_MODEL_NAME="qwen3:14b"`}
|
|
||||||
/>
|
|
||||||
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
|
||||||
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
|
||||||
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
|
||||||
</h3>
|
|
||||||
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '确保 OLLAMA_ORIGINS 设置为 * 以避免 403 错误'
|
|
||||||
: 'Add * to OLLAMA_ORIGINS to avoid 403 errors'}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '小于 10B 参数的模型通常效果不佳'
|
|
||||||
: 'Models smaller than 10B are unlikely to be strong enough'}
|
|
||||||
</li>
|
|
||||||
<li>{isZh ? '需要支持 tool_call 的模型' : 'Requires tool_call capable models'}</li>
|
|
||||||
<li>
|
|
||||||
{isZh
|
|
||||||
? '确保上下文长度大于输入 token 数,否则 Ollama 会静默截断 prompt。普通页面约需 15k token,随步骤增加。默认 4k 上下文长度无法正常工作'
|
|
||||||
: 'Ensure context length exceeds input tokens, or Ollama will silently truncate prompts. ~15k tokens for a typical page, increases with steps. Default 4k context length will NOT work'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
|
||||||
{isZh ? '建议启动参数' : 'Recommended Startup'}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
|
||||||
{isZh
|
|
||||||
? '启动 Ollama 时建议配置以下环境变量:扩大上下文窗口、允许跨域访问、监听所有网络接口。'
|
|
||||||
: 'Start Ollama with these environment variables: larger context window, allow cross-origin access, and listen on all interfaces.'}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">macOS / Linux</p>
|
|
||||||
<CodeEditor
|
|
||||||
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
|
|
||||||
Windows (PowerShell)
|
|
||||||
</p>
|
|
||||||
<CodeEditor
|
|
||||||
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Production Authentication */}
|
|
||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
<Heading id="production-authentication" className="text-2xl font-semibold mb-4">
|
||||||
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
{isZh ? '🔐 生产环境鉴权' : '🔐 Production Authentication'}
|
||||||
@@ -292,6 +227,231 @@ LLM_MODEL_NAME="qwen3:14b"`}
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-10">
|
||||||
|
<Heading id="prompt-caching">{isZh ? '主动缓存' : 'Prompt Caching'}</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh ? (
|
||||||
|
<>
|
||||||
|
一些 LLM 能从主动缓存中受益很多。由于各个供应商的主动缓存接口不同,推荐使用{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core#configuration"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
transformRequestBody
|
||||||
|
</Link>{' '}
|
||||||
|
为你的模型供应商配置缓存提示。
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
Some LLMs benefit significantly from prompt caching. Because each provider exposes
|
||||||
|
caching differently, use{' '}
|
||||||
|
<Link
|
||||||
|
href="/advanced/page-agent-core#configuration"
|
||||||
|
className="text-blue-600 dark:text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
transformRequestBody
|
||||||
|
</Link>{' '}
|
||||||
|
to add provider-specific cache hints.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<section>
|
||||||
|
<Heading id="prompt-caching-claude" level={3}>
|
||||||
|
Claude
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? 'Claude 支持全局 Automatic prompt caching。使用兼容 Claude 的代理时,只需要在请求体顶层添加 cache_control。'
|
||||||
|
: 'Claude supports global Automatic prompt caching. When using a Claude-compatible proxy, add cache_control at the top level of the request body.'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'https://your-claude-proxy.example/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'claude-sonnet-4.5',
|
||||||
|
transformRequestBody: (requestBody) => ({
|
||||||
|
...requestBody,
|
||||||
|
cache_control: { type: 'ephemeral' },
|
||||||
|
}),
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<Heading id="prompt-caching-qwen" level={3}>
|
||||||
|
{isZh ? '阿里云百炼 Qwen' : 'Alibaba Cloud Bailian Qwen'}
|
||||||
|
</Heading>
|
||||||
|
<CodeEditor
|
||||||
|
language="typescript"
|
||||||
|
code={`const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
|
apiKey: 'your-api-key',
|
||||||
|
model: 'qwen3.5-plus',
|
||||||
|
transformRequestBody: (requestBody) => {
|
||||||
|
const [systemMessage, ...restMessages] = requestBody.messages
|
||||||
|
|
||||||
|
if (systemMessage.role !== 'system' || typeof systemMessage.content !== 'string') {
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...requestBody,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
...systemMessage,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: systemMessage.content,
|
||||||
|
cache_control: { type: 'ephemeral' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
...restMessages,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="mb-10">
|
||||||
|
<Heading id="local-runtimes">{isZh ? '本地 LLMs' : 'Local LLMs'}</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
{isZh
|
||||||
|
? '通过 Ollama、LM Studio 等本地 OpenAI-compatible 运行时接入 PageAgent,实现离线或局域网部署。'
|
||||||
|
: 'Use local OpenAI-compatible runtimes such as Ollama and LM Studio with PageAgent for offline or LAN deployments.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-10">
|
||||||
|
<section>
|
||||||
|
<Heading id="requirements" level={3}>
|
||||||
|
Requirements
|
||||||
|
</Heading>
|
||||||
|
<div className="mt-4 p-5 bg-amber-50 dark:bg-amber-950/20 rounded-xl border border-amber-200 dark:border-amber-800">
|
||||||
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '务必打开 CORS,否则浏览器无法直接请求本地 LLM 服务。'
|
||||||
|
: 'Enable CORS, otherwise the browser cannot call your local LLM endpoint directly.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '将 context length 或 content length 至少设置为 8000。普通页面常常需要 15k token 左右,默认 4k 很容易被截断。'
|
||||||
|
: 'Set context length or content length to at least 8000. A typical page often needs around 15k tokens, so the default 4k usually truncates prompts.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh ? '需要支持 tool_call 的模型。' : 'Use a model with tool_call support.'}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? '小于 10B 参数的模型通常效果不佳。'
|
||||||
|
: 'Models smaller than 10B are usually not strong enough.'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Heading id="local-configuration" level={3}>
|
||||||
|
{isZh ? '基础配置' : 'Basic Configuration'}
|
||||||
|
</Heading>
|
||||||
|
<CodeEditor
|
||||||
|
code={`// Local OpenAI-compatible runtime - no apiKey needed
|
||||||
|
const pageAgent = new PageAgent({
|
||||||
|
baseURL: 'http://localhost:11434/v1',
|
||||||
|
model: 'qwen3:14b'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Or connect to LM Studio
|
||||||
|
const lmStudioAgent = new PageAgent({
|
||||||
|
baseURL: 'http://127.0.0.1:1234/v1',
|
||||||
|
model: 'qwen/qwen3.5-27b'
|
||||||
|
});
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Heading id="ollama" level={3}>
|
||||||
|
Ollama
|
||||||
|
</Heading>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{isZh
|
||||||
|
? '已在 Ollama 0.15 + qwen3:14b (RTX3090 24GB) 上测试通过。'
|
||||||
|
: 'Tested on Ollama 0.15 with qwen3:14b (RTX3090 24GB).'}
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`LLM_BASE_URL="http://localhost:11434/v1"
|
||||||
|
LLM_MODEL_NAME="qwen3:14b"`}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
|
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
||||||
|
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{isZh
|
||||||
|
? '如果浏览器侧请求失败,优先检查 Ollama 是否已按上面的要求开启 CORS。'
|
||||||
|
: 'If browser-side requests fail, check whether Ollama has CORS enabled as required above.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
{isZh ? '建议启动参数' : 'Recommended Startup'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">
|
||||||
|
{isZh
|
||||||
|
? '启动 Ollama 时建议同时放大上下文窗口并开启跨域访问。'
|
||||||
|
: 'When starting Ollama, increase the context window and enable cross-origin access.'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400">
|
||||||
|
macOS / Linux
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`OLLAMA_CONTEXT_LENGTH=64000 OLLAMA_HOST=0.0.0.0:11434 OLLAMA_ORIGINS="*" ollama serve`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 pt-2">
|
||||||
|
Windows (PowerShell)
|
||||||
|
</p>
|
||||||
|
<CodeEditor
|
||||||
|
code={`$env:OLLAMA_CONTEXT_LENGTH=64000; $env:OLLAMA_HOST="0.0.0.0:11434"; $env:OLLAMA_ORIGINS="*"; ollama serve`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Heading id="lm-studio" level={3}>
|
||||||
|
LM Studio
|
||||||
|
</Heading>
|
||||||
|
<CodeEditor
|
||||||
|
code={`LLM_BASE_URL="http://127.0.0.1:1234/v1"
|
||||||
|
LLM_MODEL_NAME="qwen/qwen3.5-27b"`}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-950/20 rounded-lg border border-amber-200 dark:border-amber-800">
|
||||||
|
<h3 className="font-semibold text-amber-900 dark:text-amber-200 mb-2">
|
||||||
|
{isZh ? '⚠️ 注意事项' : '⚠️ Important Notes'}
|
||||||
|
</h3>
|
||||||
|
<ul className="text-sm text-gray-700 dark:text-gray-300 space-y-2 list-disc pl-5">
|
||||||
|
<li>
|
||||||
|
{isZh
|
||||||
|
? 'Agent 必须启用 disableNamedToolChoice,否则 tool_choice 参数会报错。'
|
||||||
|
: 'Enable disableNamedToolChoice in the agent config, otherwise the tool_choice parameter may fail.'}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user