Feishu Portfolio Launch
What This Skill Does
In one sentence: Give me a Feishu Bitable link, I'll build a portfolio website, and automate handling the 'Feishu temporary link expiration' problem.
Default deliverables:
index.htmlor static frontend pageapi/videos.jsonapi/covers.jsonapi/portfolio.jsonrefresh.py.github/workflows/refresh.yml- Animated frontend interaction layer (CSS / JS / Canvas / Three.js all acceptable)
Built-in template resources:
Target outcome:
- Website deployed on GitHub Pages
- Feishu media links automatically refreshed every 12 hours via GitHub Actions
- User doesn't need to keep their PC running
- Pages not purely static; at least a complete set of entrance / scroll / hover / page-transition animations
Applicable Scenarios
✅ Suitable for:
- AI video / image portfolios
- Photography, design, creative sample libraries
- Client case showcase sites
- Any scenario where 'content is in Feishu attachments, frontend is only responsible for display'
❌ Not suitable for:
- Textual blogs
- E-commerce, payment, login systems
- Websites requiring database writes and backend business logic
Core Principle
Never treat a Feishu temporary URL as a permanent resource.
Feishu attachment playback/download URLs expire. The correct approach is not to have users run scripts periodically on their local machines, but:
- Read the latest records from Feishu Bitable
- Call the Feishu API to resolve attachment
file_tokeninto temporary URLs - Generate
api/*.json - Commit back to the GitHub repository
- Let GitHub Pages automatically redeploy
That is:
Feishu Bitable -> refresh.py -> api/*.json -> GitHub Pages
Execution Flow
Step 0: Environment Check
First check if the local environment has the required tools for initialization:
for cmd in node python3 gh git; do
which "$cmd" >/dev/null 2>&1 && echo "OK $cmd" || echo "MISS $cmd"
done
lark-cli --version >/dev/null 2>&1 && echo "OK lark-cli" || echo "MISS lark-cli"
gh auth status >/dev/null 2>&1 && echo "OK GitHub auth" || echo "MISS GitHub auth"
If tools are missing, ask the user to install them.
If gh is not logged in, complete GitHub login first.
Step 1: Get the Feishu Base and the Correct Data Table
Ask the user for the Feishu Bitable link, e.g.:
https://my.feishu.cn/base/AbCdEfGhIjk
Parse the app_token from the URL.
Then list all tables:
lark-cli api GET "/open-apis/bitable/v1/apps/${APP_TOKEN}/tables"
Do not default to selecting the first table.
Many Bases contain both an "empty template table" and a "real material table with works." Continue to read records and verify:
- Which table has actual attachment fields
- Which table has the correct number of records
- Which table has field names that match the content the user wants to display
Prioritize finding these fields:
- Video field:
Sample/Video/Attachment - Cover field:
Cover/Cover Image/Poster/Image - Title field:
Content/Title/Work Name - Category field:
Type/Category - Duration field:
Duration - Tool field:
AI Tool/Tool - Sort field:
Order
If a table only has template fields like Text / Single Select / Date / Attachment, continue to the next table; do not generate the website.
Step 2: Verify Feishu Permissions
For GitHub Actions to refresh automatically, the Feishu app must at least be able to:
- Read Bitable records
- Read attachment media links
The execution plan should explicitly depend on these 4 secrets:
LARK_APP_IDLARK_APP_SECRETLARK_BASE_TOKENLARK_TABLE_ID
If the user already has a working Feishu app configuration on their machine, it can be reused. Otherwise, clearly instruct the user to add permissions and obtain keys from the Feishu Open Platform.
Step 3: Export Structured Work Data
Do not just extract a list of file_tokens.
Construct structured data directly consumable by the frontend, at least containing:
- title
- categories
- duration
- tools
- order
- video_token
- cover_token
Recommended output:
api/portfolio.json: Complete array of worksapi/videos.json:video_token -> tmp_download_urlapi/covers.json:cover_token -> tmp_download_url
Structure examples can be directly referenced from assets/portfolio.sample.json in the repository.
The frontend can continue to use static HTML + JS, or be fully rendered from portfolio.json.
Step 4: Generate Frontend Page
Generate a static page by default, with the following requirements:
- Responsive work grid
- Category filtering
- Click card to play
- Full-screen playback
- Only allow one video to play at a time
- Cover preferably uses the Feishu cover field; if no cover, gracefully fallback to no poster
Animations are required to be delivered together by default; do not just create a "functional" static page:
- At least one set of entrance animations on the first screen, e.g., title, description, stats fade-up in sequence
- Navigation should allow blur / background / border changes after scrolling
- Cards should have at least a lightweight hover feedback, e.g., lift, scale, overlay, title color change
- Page or section transitions should have animations, not abrupt cuts
- Information sections like About / Contact / Skills should preferably use scroll reveal
- If the user desires stronger visual performance, Canvas or Three.js background animations can be added
Implementation boundaries:
- Prioritize native CSS transitions / keyframes / IntersectionObserver
- Use
requestAnimationFrameonly for continuously moving backgrounds - If Three.js is used, ensure it doesn't cause blank screens, freezes, or obscure main content on mobile
- Animations must not affect video playback, filtering, modals, mobile scrolling, or first-screen readability
- Provide graceful degradation; text and content should remain visible if scripts fail
Data binding principle:
- HTML should not hardcode expired URLs
- Page should fetch
/api/videos.jsonand/api/covers.jsonat runtime - Or directly fetch
/api/portfolio.jsonto render cards
If the user has an existing site, prioritize preserving the current visual style, only replacing the data source and refresh mechanism.
If the user explicitly mentions referencing an existing site style, prioritize reusing these animation types:
- Hero text entrance
- Sticky filter / nav scroll state transitions
- Masonry card hover effects
- Page fade-in/fade-out transitions
- About page stagger reveal
- Optional Three.js / Canvas background motion
Step 5: Create Repository and Deploy GitHub Pages
Standard flow:
gh repo create <repo-name> --public --source=. --remote=origin --push
Then confirm the Pages source:
- branch:
main - path:
/
Check:
gh api repos/<owner>/<repo>/pages
Must confirm:
- Pages is enabled
cnameis correct (if custom domain)- Source points to
mainand root directory
Step 6: Generate Auto-Refresh Script
The refresh script should follow this route:
- Exchange
app_id + app_secretfortenant_access_token - Read Bitable records
- Extract video and cover attachment tokens from records
- Batch call
batch_get_tmp_download_url - Write to
api/videos.json,api/covers.json,api/portfolio.json
Key requirements:
- Support paginated reading of records
- Batch requests in groups of 5 tokens
- Do not hardcode the token list in the script
- Prefer dynamic generation from actual table records
- Allow multiple candidate field names
Recommended filename:
refresh.py
Prefer directly copying the skill's built-in template:
cp ~/.claude/skills/feishu-portfolio-launch/assets/refresh.py ./refresh.py
Then only modify the field candidate arrays; do not hardcode specific tokens or URLs.
Step 7: Generate GitHub Actions Workflow
Do not use local launchd as the default.
The default must be GitHub Actions.
Workflow requirements:
- Support
workflow_dispatch - Support
schedule, default execution every 12 hours - Execute
python refresh.py - Automatically commit
api/videos.json,api/covers.json,api/portfolio.json
Standard secrets:
LARK_APP_IDLARK_APP_SECRETLARK_BASE_TOKENLARK_TABLE_ID
Standard file:
.github/workflows/refresh.yml
Prefer directly copying the skill's built-in template:
mkdir -p .github/workflows
cp ~/.claude/skills/feishu-portfolio-launch/assets/refresh.yml .github/workflows/refresh.yml
Step 8: Configure Secrets in Repository
If the current environment already has these values, and the user explicitly asks to apply them, directly configure:
gh secret set LARK_APP_ID -R <owner>/<repo>
gh secret set LARK_APP_SECRET -R <owner>/<repo>
gh secret set LARK_BASE_TOKEN -R <owner>/<repo>
gh secret set LARK_TABLE_ID -R <owner>/<repo>
Then verify:
gh secret list -R <owner>/<repo>
If any value is missing, stop here and clearly tell the user which one is missing; do not fabricate.
Step 9: Manually Trigger and Verify the Full Chain
After completing configuration, manually run the workflow once:
gh workflow run refresh.yml -R <owner>/<repo>
gh run list -R <owner>/<repo> --limit 3
gh run watch <run-id> -R <owner>/<repo> --exit-status
Then check the online result:
python3 - <<'PY'
import json, urllib.request, time
url = f'https://<domain-or-pages-url>/api/videos.json?t={int(time.time())}'
with urllib.request.urlopen(url, timeout=20) as r:
data = json.load(r)
print(data.get('_refreshed_at'))
print(data.get('_count'))
PY
Then spot-check 1 video:
- Status code should be
206or200 content-typeshould bevideo/mp4or a playable media type
If the online api/videos.json still has an old timestamp:
- First check if Pages deployment is complete
- Then check if the workflow actually committed new JSON
- Then confirm if Pages source is pointing to the correct branch and directory
Step 10: Domain Binding
If the user wants a custom domain, proceed with:
CNAMEfile- GitHub Pages custom domain settings
- DNS configuration guidance
This part follows the standard GitHub Pages flow and does not need to be coupled with Feishu logic.
Default Explanation to Users
When encountering "website can open but video won't play," first diagnose:
- Is
api/videos.jsonstill old? - Are the Feishu URLs inside expired?
- Did the auto-refresh workflow fail?
- Has Pages not redeployed yet?
By default, do not first suspect the player or the frontend styles.
If the user says "the page is too static" or "I want it to feel more alive like a portfolio," add animation layers by default, rather than just changing colors and layout.
Success Criteria
Upon completing this Skill, at least the following must be met:
- Website is live
- GitHub Pages is accessible
- Videos are playable
- Repository has auto-refresh workflow
- Repository has the 4
LARK_*secrets - Manual trigger of the workflow succeeded
- Online
api/videos.jsonhas a new_refreshed_attimestamp
What Not to Default To
- Do not default to generating local
launchd - Do not default to relying on the user's Mac being on indefinitely
- Do not hardcode all
file_tokens - Do not proceed with only the first table
- Do not generate only
videos.json, ignoring covers and structured records
Reference Implementation
If a verified approach is needed, first directly reuse the skill repository's built-in templates:
assets/refresh.pyassets/refresh.ymlassets/portfolio.sample.json
In the actual project, the following files will eventually be generated:
refresh.py.github/workflows/refresh.ymlapi/videos.jsonapi/covers.jsonapi/portfolio.json
Prioritize reusing the existing repository pattern, then adapt according to the user's site style.