# TripStar plans a trip by returning a task id and streaming the work

> TripStar is a Vue and FastAPI travel planner built on the HelloAgents framework, where a supervisor agent fans out to Xiaohongshu scraping, weather and hotel agents, then merges the results into one nested JSON plan. The interesting engineering is not the itinerary, it is the async task layer, the geocoding fallback chain and the five-step repair ladder that makes an LLM's malformed JSON usable.

**1sdv/TripStar** — 旅途星辰 (TripStar)是一个创新的 AI 文旅智能体应用，旨在解决用户在规划旅行时面临的各种问题，为提供一站式旅游攻略而生。

- Repository: https://github.com/1sdv/TripStar
- Stars: 2,281 · Forks: 272
- Language: Vue
- License: GPL-2.0
- Published: 2026-09-29 · Updated: 2026-09-29 · Language: en
- Canonical page: https://hysenlabs.com/projects/1sdv-tripstar

## The plan endpoint hands back a task id, not an itinerary

Most of the design pressure in TripStar comes from one fact: a long LLM generation behind a synchronous HTTP request times out at the gateway. The architecture diagram in the README shows the fix. A client POSTs to /api/trip/plan with a city, a number of days and preferences, and the route immediately returns a task_id and a ws_url. The long reasoning job is pushed into the background with asyncio.create_task, and the frontend subscribes over a WebSocket at /ws/{task_id} to receive processing and progress updates. A lighter polling path exists as well: GET /api/trip/status/{task_id} is hit every three seconds until the status turns to completed. The route layer in api/routes/trip.py also persists state to disk on completion, which is why the compose file mounts a named volume at /app/backend/data. The practical consequence for an adopter is that this is not a request-response API. Anything you build on top has to treat planning as a long-running job with its own lifecycle.

## Three agents run concurrently, then one prompt merges them

The core of the system is a two-stage workflow, and the README's sequence diagram draws it precisely. In the concurrent stage, marked as optimised with asyncio.gather, three things happen at once. The attraction search asks xhs_service.py for candidates and then hands raw travelogue text to the LLM with a prompt demanding a JSON array of attractions, which comes back with fields such as name and duration. The weather agent calls a tool proxied through map_dispatcher.py, and the hotel agent calls a POI text search tool the same way. In the serial stage, the supervisor concatenates attractions, weather and hotels into a single planner prompt and asks for one nested JSON document covering the itinerary, the budget and everything else. The diagram marks that call as a high-risk operation, and it is the project's central fragility: the whole product depends on a model returning a large structured document correctly.

## A five-step repair ladder makes the model output usable

Because that final JSON is untrusted, the planner runs it through a tolerant parser, and the README names the steps in order. First it cleans stray characters. Then it repairs unescaped quotes, which is the classic failure when a model writes a JSON string containing a quotation mark. Then it attempts a truncation repair by closing unbalanced brackets. Then it falls back to brute-force extraction of the structure. Only if all four fail does it ask the LLM itself to patch the result. This is an honest piece of engineering rather than a hidden hack, and it also tells you what to expect from the system: on a long itinerary you will hit the repair path reasonably often, and when you do, the budget numbers and day-by-day structure are the fields most likely to be subtly wrong rather than absent. If you are building on this, validate the shape of the returned plan before you render it, because nothing in the description of the pipeline suggests the app checks the semantics.

## Xiaohongshu notes are scraped, purified by the model, then geocoded

The attraction data has an unusual provenance. xhs_service.py reaches Xiaohongshu through an XhsNativeClient that signs requests natively, with a server-side rendered page scrape as the fallback path, and the raw travelogue posts are thrown at the LLM to extract attraction names, real reviews, visit durations and whether advance booking is required. Coordinates are then filled in per attraction with geocode_unified, which prefers Google and drops down to the AMap REST service on failure. Images are handled separately and lazily: once the plan is rendered, the frontend calls GET /api/poi/photo for each attraction name, and the backend searches Xiaohongshu for a recent post and returns the direct URL of its first image. The booking reminder feature falls out of this: the model is asked to flag attractions mentioned as needing reservation, such as the Palace Museum, and those notes are marked in the itinerary card. The cost of the design is the one configuration line that matters most, XHS_COOKIE, which the environment example shows holding a personal session.

## Two map engines, one dispatcher, automatic fallback

Map handling is delegated to a single file, map_dispatcher.py, which is what keeps the dual-engine promise from spreading through the codebase. Google is preferred and AMap is the fallback: geocode_unified tries Google first, and the weather path does the same, requesting the AMap weather REST interface when the Google call fails. The README frames the routing rule geographically, using Google Maps abroad and falling back to AMap at home, and the frontend switches its base map and marker colours to match. The itinerary route is drawn as real start-to-attraction-to-end polylines from actual latitude and longitude, which is the part that depends most on the geocoder getting the right city. If you enable the Google engine, the README says the Google Cloud project must have Geocoding API and Places API (New) enabled, along with a directions API, and it also offers an optional GOOGLE_MAPS_PROXY variable used only by the backend Google calls.

## Deploy with compose, and expect two AMap keys plus a JSCode

The runtime requirements are Python 3.10+, Node.js 18+, an LLM key from any OpenAI-compatible provider, and map credentials. AMap needs two keys rather than one: a web service key for backend REST calls such as geocoding, POI and weather, and a web-end JS API key for the frontend map. The security JSCode is the third piece, and the README is emphatic that it must go into the .env as VITE_AMAP_SECURITY_JS_CODE, because the build replaces a placeholder in index.html with it, and a key written straight into index.html is the failure mode to avoid. The documented path is to copy the environment template and build:

```bash
docker-compose up -d --build
```

The LLM section of the template looks like this, with a ten minute timeout because a long itinerary takes minutes:

```yaml
LLM_API_KEY=your_api_key
LLM_BASE_URL=https://your-openai-compatible-endpoint/v1
LLM_MODEL_ID=your_model
LLM_TIMEOUT=600
```

The service listens on 7860, mapped in docker-compose.yaml, and the image is built in two stages: a node:18-slim stage builds the Vue app with Vite, and a python:3.10-slim stage carries the backend with gunicorn and uvicorn installed through uv, plus Node in the runtime because the Xiaohongshu signing engine is a Node program. That last detail is the one that surprises people: the container is not pure Python.

## Preference memory is six environment variables and a decay factor

The memory module is off by default, and when you turn it on it is a small weighted recall system rather than a vector store, which is a refreshing choice. ENABLE_USER_MEMORY=false gates the whole thing. MEMORY_THRESHOLD=2.0 decides what counts as a stable preference worth recording, MIN_INIT_WEIGHT=4.0 is the starting weight a new memory gets, and MEMORY_DECAY_FACTOR=0.97 shrinks every weight slightly on each pass, so a preference you expressed once fades and one you express repeatedly rises. MEMORY_MAX_RECALL=10 caps how many are injected into the agent prompt, and MEMORY_MAX_SINGLE_CONTENT=120 limits each stored memory's length, which keeps the prompt bounded. MEMORY_USE_SQLITE=false switches the store. The design consequence is worth understanding: with a 0.97 factor, a memory needs several mentions to survive, and only ten make it into any single plan. For someone planning two trips a year, that tuning is close to pointless. For someone planning monthly, it is the difference between re-explaining your hotel style every time and not.

## Failure modes: a personal cookie, a China mirror, and a skipped type check

Three things will cost you time. First, XHS_COOKIE is a logged-in session belonging to a person, and the compose file passes it into the same container as your LLM key; the environment example shows it holding a1, web_session and webId values. Second, the Dockerfile pins package mirrors, using npmmirror for npm and the Aliyun PyPI mirror for uv and pip, which will be slow or blocked from outside China and means editing the Dockerfile to install anywhere else. Third, the frontend build deliberately skips vue-tsc, with a comment saying type errors do not affect runtime, so the build will not tell you about type problems in the Vue code. There is also an honesty point in the README's own warning: a local deployment works, but the full feature set only appears once the keys are configured. And a design limit, not a bug: since each image is fetched one request at a time from the frontend after the plan renders, a ten-attraction itinerary makes ten sequential calls to /api/poi/photo, which is why the diagram marks that part as lazy loading.

## What it is competing with: a guide site and a single prompt

There are two obvious alternatives. A travel guide site gives you curated, human-written itineraries with no keys, no cookies and no model in the loop, and it is instantly better if you want a reliable answer for a popular city. A single LLM prompt does roughly what TripStar does for one city, minus the map engine, the budget panel and the knowledge graph, but it takes about thirty seconds instead of minutes and has no Xiaohongshu dependency. What TripStar adds over both is the pipeline: concurrent agent stages, geocoded real coordinates, a knowledge graph built from the finished plan and rendered with ECharts, a floating Q&A window that holds the full itinerary as context, and multi-city trips with per-city weather and separate inter-city transport accounting in the budget. If your requirement is a nice one-off plan, the single prompt wins on effort. If your requirement is a product with maps, budgets and follow-up questions, the architecture in the README is the part worth stealing, and it is readable.

## Conclusion

Adopt TripStar if you want to see what a supervisor-plus-tools agent architecture looks like end to end, with the concurrency, the fallback chain and the JSON repair ladder all visible in the source. Do not adopt it as a booking or routing system, and think twice before pasting a real XHS_COOKIE into a container that also holds your LLM key. Verify four things first: that the Google Cloud project has Geocoding API and Places API (New) enabled if you want the Google engine, that your LLM endpoint can return a large nested JSON object rather than prose, that the AMap JSCode is injected through VITE_AMAP_SECURITY_JS_CODE rather than hand-edited into index.html, and that the 0.97 memory decay suits how often you actually plan trips. The project is GPL-2.0, ships no GitHub releases, and its last push was 2026-09-16.

## FAQ

### How do I run TripStar locally?

Copy the environment template to a .env at the repository root, fill in the LLM and map keys, then build and start the service with docker-compose up -d --build. The container listens on port 7860.

### Which API keys does TripStar need?

An LLM key for any OpenAI-compatible provider, two AMap keys because one serves the backend REST calls and the other renders the frontend map, plus the AMap JSCode injected as VITE_AMAP_SECURITY_JS_CODE. A Google Maps key is optional and only needed for the Google engine.

### Why does TripStar need a Xiaohongshu cookie?

Attraction notes and images are pulled from Xiaohongshu, and XHS_COOKIE in the environment template holds a personal session with a1, web_session and webId values. The service connects through a native signing client with a server-side rendered scrape as fallback.

### How does the TripStar preference memory work?

It is a weighted recall system with decay. MEMORY_THRESHOLD and MIN_INIT_WEIGHT decide what gets stored and at what weight, MEMORY_DECAY_FACTOR=0.97 shrinks weights over time, MEMORY_MAX_RECALL=10 caps how many are injected into the prompt, and MEMORY_USE_SQLITE switches the store. It is disabled by default.

### How long does a TripStar plan take to generate?

Minutes rather than seconds, which is why /api/trip/plan returns a task_id immediately and the work runs in the background. The frontend can subscribe over the WebSocket at /ws/{task_id} or poll /api/trip/status/{task_id} every three seconds.

## Sources

- [1sdv/TripStar on GitHub](https://github.com/1sdv/TripStar)
- [Issues](https://github.com/1sdv/TripStar/issues)
- [License: GPL-2.0](https://github.com/1sdv/TripStar/blob/main/LICENSE)
- [README](https://github.com/1sdv/TripStar/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/1sdv-tripstar
